pandas 基础

《利用 python 进行数据分析*第二版》

1
2
3
4
5
6
import pandas as pd
from pandas import Series, DataFrame
import numpy as np
from numpy import nan as NA
import sqlite3
import re

基础部分

数据结构

Series 数据结构

Series 数据结构就是列表和字典的结合体,下面看一下具体演示

1
2
3
# 创建
obj = pd.Series([4, 7, -5, 3])
obj
0    4
1    7
2   -5
3    3
dtype: int64
1
2
# 获取索引
obj.index
RangeIndex(start=0, stop=4, step=1)
1
2
# 获取值
obj.values
array([ 4,  7, -5,  3], dtype=int64)
1
2
3
# 创建的同时制定索引
obj2 = pd.Series([4, 7, -5, 3], index=['d', 'b', 'a', 'c'])
obj2
d    4
b    7
a   -5
c    3
dtype: int64
1
obj2.index
Index(['d', 'b', 'a', 'c'], dtype='object')
1
obj2.values
array([ 4,  7, -5,  3], dtype=int64)
1
2
3
4
# 通过字典进行创建
sdata = {'Ohio': 35000, 'Texas': 71000, 'Oregon': 16000, 'Utah': 5000}
obj3 = pd.Series(sdata)
obj3
Ohio      35000
Texas     71000
Oregon    16000
Utah       5000
dtype: int64

可以传入排好序的字典的键以改变顺序
sdata 中跟 states 索引相匹配的那3个值会被找出来并放到相应的位置上,但由于”California”所对应的 sdata 值找不到,所以其结果就为 NaN(即“非数字”(not a number),在 pandas 中,它用于表示缺失或 NA 值)。因为‘Utah’不在 states 中,它被从结果中除去。

1
2
3
states = ['California', 'Ohio', 'Oregon', 'Texas']
obj4 = pd.Series(sdata, index=states)
obj4
California        NaN
Ohio          35000.0
Oregon        16000.0
Texas         71000.0
dtype: float64
1
2
# 判断索引是否存在
'ding' in obj4
False
1
'Ohio' in obj4
True

Series 对象本身及其索引都有一个 name 属性,该属性跟pandas其他的关键功能关系非常密切:

1
2
3
obj4.name = 'population'
obj4.index.name = 'state'
obj4
state
California        NaN
Ohio          35000.0
Oregon        16000.0
Texas         71000.0
Name: population, dtype: float64

Series 的索引可以通过赋值的方式就地修改

1
2
obj4.index = ['Bob', 'Steve', 'Jeff', 'Ryan']
obj4
Bob          NaN
Steve    35000.0
Jeff     16000.0
Ryan     71000.0
Name: population, dtype: float64

DataFrame 数据结构

DataFrame是一个表格型的数据结构,它含有一组有序的列,每列可以是不同的值类型(数值、字符串、布尔值等)。DataFrame既有行索引也有列索引,它可以被看做由Series组成的字典(共用同一个索引)。DataFrame中的数据是以一个或多个二维块存放的(而不是列表、字典或别的一维数据结构)。

columns 相当于 sql 中的字段, index 相当于 sql 中的索引

建DataFrame的办法有很多,最常用的一种是直接传入一个由等长列表或NumPy数组组成的字典

1
2
3
4
5
data = {'state': ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada', 'Nevada'],
'year': [2000, 2001, 2002, 2001, 2002, 2003],
'pop': [1.5, 1.7, 3.6, 2.4, 2.9, 3.2]}
frame = pd.DataFrame(data)
frame
state year pop
0 Ohio 2000 1.5
1 Ohio 2001 1.7
2 Ohio 2002 3.6
3 Nevada 2001 2.4
4 Nevada 2002 2.9
5 Nevada 2003 3.2
1
2
# 读取前 5 行,数据量大的时候非常有用
frame.head()
state year pop
0 Ohio 2000 1.5
1 Ohio 2001 1.7
2 Ohio 2002 3.6
3 Nevada 2001 2.4
4 Nevada 2002 2.9

如果指定了列序列,则DataFrame的列就会按照指定顺序进行排列

1
2
# 制定列顺序
pd.DataFrame(data, columns=['year', 'state', 'pop'])
year state pop
0 2000 Ohio 1.5
1 2001 Ohio 1.7
2 2002 Ohio 3.6
3 2001 Nevada 2.4
4 2002 Nevada 2.9
5 2003 Nevada 3.2
1
2
3
# 同时制定 列名 和 索引名
frame2 = pd.DataFrame(data, columns=['year', 'state', 'pop', 'debt'],index=['one', 'two', 'three', 'four', 'five', 'six'])
frame2
year state pop debt
one 2000 Ohio 1.5 NaN
two 2001 Ohio 1.7 NaN
three 2002 Ohio 3.6 NaN
four 2001 Nevada 2.4 NaN
five 2002 Nevada 2.9 NaN
six 2003 Nevada 3.2 NaN
1
2
# 获取 列名
frame2.columns
Index(['year', 'state', 'pop', 'debt'], dtype='object')
1
2
# 获取某一列
frame2['state']
one        Ohio
two        Ohio
three      Ohio
four     Nevada
five     Nevada
six      Nevada
Name: state, dtype: object
1
frame2.state
one        Ohio
two        Ohio
three      Ohio
four     Nevada
five     Nevada
six      Nevada
Name: state, dtype: object
1
2
# 选取某一行
frame2.loc['one']
year     2000
state    Ohio
pop       1.5
debt      NaN
Name: one, dtype: object
1
2
3
# 赋值
frame2['debt'] = 16.5
frame2
year state pop debt
one 2000 Ohio 1.5 16.5
two 2001 Ohio 1.7 16.5
three 2002 Ohio 3.6 16.5
four 2001 Nevada 2.4 16.5
five 2002 Nevada 2.9 16.5
six 2003 Nevada 3.2 16.5

将列表或数组赋值给某个列时,其长度必须跟DataFrame的长度相匹配。如果赋值的是一个Series,就会精确匹配DataFrame的索引,所有的空位都将被填上缺失值:

1
2
3
val = pd.Series([-1.2, -1.5, -1.7], index=['two', 'four', 'five'])
frame2['debt'] = val
frame2
year state pop debt
one 2000 Ohio 1.5 NaN
two 2001 Ohio 1.7 -1.2
three 2002 Ohio 3.6 NaN
four 2001 Nevada 2.4 -1.5
five 2002 Nevada 2.9 -1.7
six 2003 Nevada 3.2 NaN

为不存在的列赋值会创建出一个新列。关键字del用于删除列。

1
2
3
# 创建一个新列
frame2['test'] = frame2.state == 'Ohio'
frame2
year state pop debt test
one 2000 Ohio 1.5 NaN True
two 2001 Ohio 1.7 -1.2 True
three 2002 Ohio 3.6 NaN True
four 2001 Nevada 2.4 -1.5 False
five 2002 Nevada 2.9 -1.7 False
six 2003 Nevada 3.2 NaN False
1
2
3
# 删除一列
del frame2['test']
frame2
year state pop debt
one 2000 Ohio 1.5 NaN
two 2001 Ohio 1.7 -1.2
three 2002 Ohio 3.6 NaN
four 2001 Nevada 2.4 -1.5
five 2002 Nevada 2.9 -1.7
six 2003 Nevada 3.2 NaN

另一种常见的数据形式是嵌套字典:
如果嵌套字典传给DataFrame,pandas就会被解释为:外层字典的键作为列,内层键则作为行索引:

1
2
3
pop = {'Nevada': {2001: 2.4, 2002: 2.9}, 'Ohio': {2000: 1.5, 2001: 1.7, 2002: 3.6}}
frame3 = pd.DataFrame(pop)
frame3
Nevada Ohio
2000 NaN 1.5
2001 2.4 1.7
2002 2.9 3.6

如果设置了 DataFrame 的 index 和 columns 的 name 属性,则这些信息也会被显示出来:

1
2
frame3.index.name = 'year'; frame3.columns.name = 'state'
frame3
state Nevada Ohio
year
2000 NaN 1.5
2001 2.4 1.7
2002 2.9 3.6
1
2
# 获取值
frame3.values
array([[nan, 1.5],
       [2.4, 1.7],
       [2.9, 3.6]])

索引对象

pandas的索引对象负责管理轴标签和其他元数据(比如轴名称等)。构建Series或DataFrame时,所用到的任何数组或其他序列的标签都会被转换成一个Index:
DataFrame 的 index 和 columns 都是索引对象

1
2
obj = pd.Series(range(3), index=['a', 'b', 'c'])
obj.index
Index(['a', 'b', 'c'], dtype='object')
1
frame3
state Nevada Ohio
year
2000 NaN 1.5
2001 2.4 1.7
2002 2.9 3.6
1
frame3.columns
Index(['Nevada', 'Ohio'], dtype='object', name='state')
1
frame3.index
Int64Index([2000, 2001, 2002], dtype='int64', name='year')

与python的集合不同,pandas的Index可以包含重复的标签:

1
2
dup_labels = pd.Index(['foo', 'foo', 'bar', 'bar'])
dup_labels
Index(['foo', 'foo', 'bar', 'bar'], dtype='object')

Index 索引对象有一些方法和属性,不怎么用

1
dup_labels.unique()
Index(['foo', 'bar'], dtype='object')

Index 方法和属性

基本功能

重新索引 reindex

pandas对象的一个重要方法是reindex,其作用是创建一个新对象,它的数据符合新的索引。

1
2
obj = pd.Series([4.5, 7.2, -5.3, 3.6], index=['d', 'b', 'a', 'c'])
obj
d    4.5
b    7.2
a   -5.3
c    3.6
dtype: float64

用该Series的reindex将会根据新索引进行重排。如果某个索引值当前不存在,就引入缺失值:

1
2
obj2 = obj.reindex(['a', 'b', 'c', 'd', 'e'])
obj2
a   -5.3
b    7.2
c    3.6
d    4.5
e    NaN
dtype: float64

重新索引时可能需要做一些插值处理。method选项即可达到此目的,例如,使用 ffill 可以实现前向值填充

1
2
obj3 = pd.Series(['blue', 'purple', 'yellow'], index=[0, 2, 4])
obj3
0      blue
2    purple
4    yellow
dtype: object
1
obj3.reindex(range(6), method='ffill')
0      blue
1      blue
2    purple
3    purple
4    yellow
5    yellow
dtype: object

借助 DataFrame,reindex 可以修改(行)索引和列。只传递一个序列时,会重新索引结果的行:

1
2
frame = pd.DataFrame(np.arange(9).reshape((3, 3)), index=['a', 'c', 'd'], columns=['Ohio', 'Texas', 'California'])
frame
Ohio Texas California
a 0 1 2
c 3 4 5
d 6 7 8
1
2
# 重建行索引
frame.reindex(['a', 'b', 'c', 'd'])
Ohio Texas California
a 0.0 1.0 2.0
b NaN NaN NaN
c 3.0 4.0 5.0
d 6.0 7.0 8.0
1
2
# 重建列索引
frame.reindex(columns = ['Texas', 'Utah', 'California'])
Texas Utah California
a 1 NaN 2
c 4 NaN 5
d 7 NaN 8

reindex函数的各参数及说明

丢弃指定轴上的项 drop

丢弃某条轴上的一个或多个项很简单,只要有一个索引数组或列表即可。由于需要执行一些数据整理和集合逻辑,所以drop方法返回的是一个在指定轴上删除了指定值的新对象:

1
2
obj = pd.Series(np.arange(5.), index=['a', 'b', 'c', 'd', 'e'])
obj
a    0.0
b    1.0
c    2.0
d    3.0
e    4.0
dtype: float64
1
obj.drop('c')
a    0.0
b    1.0
d    3.0
e    4.0
dtype: float64

对于DataFrame,可以删除任意轴上的索引值。 drop 默认会从行标签(axis 0)删除值(删除行), 通过传递 axis=1axis='columns' 可以删除列的值

1
2
data = pd.DataFrame(np.arange(16).reshape((4, 4)), index=['Ohio', 'Colorado', 'Utah', 'New York'], columns=['one', 'two', 'three', 'four'])
data
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
Utah 8 9 10 11
New York 12 13 14 15
1
data.drop(['Ohio', 'Utah'])
one two three four
Colorado 4 5 6 7
New York 12 13 14 15
1
data.drop('two', axis=1)
one three four
Ohio 0 2 3
Colorado 4 6 7
Utah 8 10 11
New York 12 14 15

许多函数,如drop,会修改Series或DataFrame的大小或形状,可以就地修改对象,不会返回新的对象:

1
obj.drop('c', inplace=True)
1
obj
a    0.0
b    1.0
d    3.0
e    4.0
dtype: float64

索引、选取和过滤

Series索引的工作方式类似于NumPy数组的索引,只不过Series的索引值不只是整数。

1
2
obj = pd.Series(np.arange(4.), index=['a', 'b', 'c', 'd'])
obj
a    0.0
b    1.0
c    2.0
d    3.0
dtype: float64
1
obj['a']
0.0
1
obj[0]
0.0
1
obj[:2]
a    0.0
b    1.0
dtype: float64
1
2
# 利用标签的切片运算与普通的Python切片运算不同,其末端是包含的:
obj['a':'c']
a    0.0
b    1.0
c    2.0
dtype: float64
1
obj[['a', 'b', 'd']]
a    0.0
b    1.0
d    3.0
dtype: float64
1
obj[[1, 3]]
b    1.0
d    3.0
dtype: float64
1
obj[obj < 2]
a    0.0
b    1.0
dtype: float64

用一个值或序列对DataFrame进行索引其实就是获取一个或多个列:
用切片 [:] 切的是行

1
2
data = pd.DataFrame(np.arange(16).reshape((4, 4)), index=['Ohio', 'Colorado', 'Utah', 'New York'], columns=['one', 'two', 'three', 'four'])
data
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
Utah 8 9 10 11
New York 12 13 14 15
1
data['two']
Ohio         1
Colorado     5
Utah         9
New York    13
Name: two, dtype: int32
1
data[['three', 'one']]
three one
Ohio 2 0
Colorado 6 4
Utah 10 8
New York 14 12
1
2
# 切行
data["Ohio":"Utah"]
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
Utah 8 9 10 11
1
data[:2]
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7

用 loc 和 iloc 进行选取

特殊的标签运算符loc和iloc。它们可以让你用类似NumPy的标记,使用轴标签(loc)或整数索引(iloc),从DataFrame选择行和列的子集。

1
data
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
Utah 8 9 10 11
New York 12 13 14 15
1
2
# 通过标签进行选择
data.loc['Ohio', ['one', 'two']]
one    0
two    1
Name: Ohio, dtype: int32
1
data.loc['Ohio':'Utah', ['one', 'two']]
one two
Ohio 0 1
Colorado 4 5
Utah 8 9
1
2
# 通过 整数 进行选择
data.iloc[2, [0, 1]]
one    8
two    9
Name: Utah, dtype: int32
1
data.iloc[2:4, [0, 1]]
one two
Utah 8 9
New York 12 13

pandas可以勉强进行整数索引,但是会导致小bug。我们有包含0,1,2的索引,但这会引起歧义,为了进行统一,如果轴索引含有整数,数据选取总会使用标签。为了更准确,请使用loc(标签)或iloc(整数):

1
2
ser = pd.Series(np.arange(3.))
ser
0    0.0
1    1.0
2    2.0
dtype: float64
1
2
# 下面这个将会出错,因为整数索引中没有 -1 这个值, 不知道基于位置索引还是整数索引,导致歧义
ser[-1]

对于非整数索引,不会产生歧义

1
2
ser2 = pd.Series(np.arange(3.), index=['a', 'b', 'c'])
ser2[-1]
2.0

算术运算和数据对齐、在算术方法中填充值

pandas最重要的一个功能是,它可以对不同索引的对象进行算术运算。在将对象相加时,如果存在不同的索引对,则结果的索引就是该索引对的并集。对于有数据库经验的用户,这就像在索引标签上进行自动外连接。

1
2
3
s1 = pd.Series([7.3, -2.5, 3.4, 1.5], index=['a', 'c', 'd', 'e'])
s2 = pd.Series([-2.1, 3.6, -1.5, 4, 3.1],index=['a', 'c', 'e', 'f', 'g'])
s1
a    7.3
c   -2.5
d    3.4
e    1.5
dtype: float64
1
s2
a   -2.1
c    3.6
e   -1.5
f    4.0
g    3.1
dtype: float64
1
2
# 索引对齐相加,自动的数据对齐操作在不重叠的索引处引入了NA值
s1 + s2
a    5.2
c    1.1
d    NaN
e    0.0
f    NaN
g    NaN
dtype: float64

对于DataFrame,对齐操作会同时发生在行和列上

1
2
3
df1 = pd.DataFrame(np.arange(9.).reshape((3, 3)), columns=list('bcd'), index=['Ohio', 'Texas', 'Colorado'])
df2 = pd.DataFrame(np.arange(12.).reshape((4, 3)), columns=list('bde'), index=['Utah', 'Ohio', 'Texas', 'Oregon'])
df1
b c d
Ohio 0.0 1.0 2.0
Texas 3.0 4.0 5.0
Colorado 6.0 7.0 8.0
1
df2
b d e
Utah 0.0 1.0 2.0
Ohio 3.0 4.0 5.0
Texas 6.0 7.0 8.0
Oregon 9.0 10.0 11.0
1
df1 + df2
b c d e
Colorado NaN NaN NaN NaN
Ohio 3.0 NaN 6.0 NaN
Oregon NaN NaN NaN NaN
Texas 9.0 NaN 12.0 NaN
Utah NaN NaN NaN NaN

在对不同索引的对象进行算术运算时,你可能希望当一个对象中某个轴标签在另一个对象中找不到时填充一个特殊值(比如0)

1
df1.add(df2, fill_value=0)
b c d e
Colorado 6.0 7.0 8.0 NaN
Ohio 3.0 1.0 6.0 5.0
Oregon 9.0 NaN 10.0 11.0
Texas 9.0 4.0 12.0 8.0
Utah 0.0 NaN 1.0 2.0

其他算数方法:
以字母r开头,它会翻转参数。

1
2
3
test1 = Series([1, 1, 1])
test2 = Series([2, 2, 2])
test1.div(test2)
0    0.5
1    0.5
2    0.5
dtype: float64
1
test1.rdiv(test2)
0    2.0
1    2.0
2    2.0
dtype: float64

DataFrame和Series之间的运算

DataFrame和Series之间的运算,默认是横向进行广播的,如果需要在列上进行广播,就必须使用算数方法计算指定 axis='index'

1
2
frame = pd.DataFrame(np.arange(12.).reshape((4, 3)), columns=list('bde'), index=['Utah', 'Ohio', 'Texas', 'Oregon'])
frame
b d e
Utah 0.0 1.0 2.0
Ohio 3.0 4.0 5.0
Texas 6.0 7.0 8.0
Oregon 9.0 10.0 11.0
1
2
series = frame.iloc[0]
series
b    0.0
d    1.0
e    2.0
Name: Utah, dtype: float64
1
2
# 行上进行广播计算
frame + series
b d e
Utah 0.0 2.0 4.0
Ohio 3.0 5.0 7.0
Texas 6.0 8.0 10.0
Oregon 9.0 11.0 13.0
1
2
# 列上进行广播计算 , 这里出现了问题,因为计算会根据索引进行匹配导致的
frame.add(series, axis = 'index')
b d e
Ohio NaN NaN NaN
Oregon NaN NaN NaN
Texas NaN NaN NaN
Utah NaN NaN NaN
b NaN NaN NaN
d NaN NaN NaN
e NaN NaN NaN
1
2
3
# series2 = frame['b']
series2 = frame.loc[:, 'b']
series2
Utah      0.0
Ohio      3.0
Texas     6.0
Oregon    9.0
Name: b, dtype: float64
1
frame.add(series2, axis = 'index')
b d e
Utah 0.0 1.0 2.0
Ohio 6.0 7.0 8.0
Texas 12.0 13.0 14.0
Oregon 18.0 19.0 20.0

函数应用和映射

将函数应用到由各列或行所形成的一维数组上。DataFrame的apply方法即可实现此功能:

1
2
frame = pd.DataFrame(np.random.randn(4, 3), columns=list('bde'), index=['Utah', 'Ohio', 'Texas', 'Oregon'])
frame
b d e
Utah -0.618453 0.743194 -1.467120
Ohio 0.272335 0.542511 -0.458843
Texas -0.762976 0.330646 0.988022
Oregon -0.207557 0.425898 0.783251
1
2
3
f = lambda x: x.max() - x.min()
# 没一列的最大值减去最小值
frame.apply(f)
b    1.035311
d    0.412548
e    2.455142
dtype: float64

如果传递 axis='columns' 到 apply ,这个函数会在每行执行:

1
frame.apply(f, axis='columns')
Utah      2.210314
Ohio      1.001354
Texas     1.750998
Oregon    0.990809
dtype: float64

传递到apply的函数不是必须返回一个标量,还可以返回由多个值组成的Series:

1
2
3
def f1(x):
return pd.Series([x.min(), x.max()], index=['min', 'max'])
frame.apply(f1)
b d e
min -0.762976 0.330646 -1.467120
max 0.272335 0.743194 0.988022

元素级的Python函数也是可以用的。假如你想得到frame中各个浮点值的格式化字符串,使用applymap即可:

1
2
format = lambda x: '%.2f' % x
frame.applymap(format)
b d e
Utah -0.62 0.74 -1.47
Ohio 0.27 0.54 -0.46
Texas -0.76 0.33 0.99
Oregon -0.21 0.43 0.78

Series 有一个用于应用元素级函数的 map 方法

1
2
series = pd.Series([1, 2, 3])
series.map(format)
0    1.00
1    2.00
2    3.00
dtype: object

排序和排名

根据条件对数据集排序(sorting)也是一种重要的内置运算。要对行或列索引进行排序(按字典顺序),可使用 sort_index 方法,它将返回一个已排序的新对象:

1
2
obj = pd.Series(range(4), index=['d', 'a', 'b', 'c'])
obj
d    0
a    1
b    2
c    3
dtype: int64

按照索引排序 sort_index

1
obj.sort_index()
a    1
b    2
c    3
d    0
dtype: int64
1
2
frame = pd.DataFrame(np.arange(8).reshape((2, 4)), index=['three', 'one'], columns=['d', 'a', 'b', 'c'])
frame
d a b c
three 0 1 2 3
one 4 5 6 7
1
2
# 按照 0 轴排序
frame.sort_index()
d a b c
one 4 5 6 7
three 0 1 2 3
1
2
# 按照 1 轴排序
frame.sort_index(axis = 1)
a b c d
three 1 2 3 0
one 5 6 7 4

数据默认是按升序排序的,但也可以降序排序:

1
frame.sort_index(axis=1, ascending=False)
d c b a
three 0 3 2 1
one 4 7 6 5

按照值排序 sort_values

1
obj
d    0
a    1
b    2
c    3
dtype: int64
1
2
obj = pd.Series([4, 7, -3, 2])
obj.sort_values()
2   -3
3    2
0    4
1    7
dtype: int64

当排序一个DataFrame时,你可能希望根据一个或多个列中的值进行排序。将一个或多个列的名字传递给 sort_values 的 by 选项即可达到该目的:

1
2
frame = pd.DataFrame({'b': [4, 7, -3, 2], 'a': [0, 1, 0, 1]})
frame
b a
0 4 0
1 7 1
2 -3 0
3 2 1
1
frame.sort_values(by='b')
b a
2 -3 0
3 2 1
0 4 0
1 7 1
1
frame.sort_values(by=['a', 'b'])
b a
2 -3 0
0 4 0
3 2 1
1 7 1

默认情况下,rank是通过“为各组分配一个平均排名”的方式破坏平级关系的:

1
2
obj = pd.Series([7, -5, 7, 4, 2, 0, 4])
obj
0    7
1   -5
2    7
3    4
4    2
5    0
6    4
dtype: int64
1
obj.rank()
0    6.5
1    1.0
2    6.5
3    4.5
4    3.0
5    2.0
6    4.5
dtype: float64

也可以根据值在原数据中出现的顺序给出排名:

1
obj.rank(method='first')
0    6.0
1    1.0
2    7.0
3    4.0
4    3.0
5    2.0
6    5.0
dtype: float64

你也可以按降序进行排名:

1
obj.rank(ascending=False, method='first')
0    1.0
1    7.0
2    2.0
3    3.0
4    5.0
5    6.0
6    4.0
dtype: float64

rank 方法在 DataFrame 可以在行或列上计算排名:

1
2
frame = pd.DataFrame({'b': [4.3, 7, -3, 2], 'a': [0, 1, 0, 1], 'c': [-2, 5, 8, -2.5]})
frame
b a c
0 4.3 0 -2.0
1 7.0 1 5.0
2 -3.0 0 8.0
3 2.0 1 -2.5
1
frame.rank(axis='columns')
b a c
0 3.0 2.0 1.0
1 3.0 1.0 2.0
2 1.0 2.0 3.0
3 3.0 2.0 1.0

排名时用于破坏平级关系的方法

带有重复标签的轴索引

重复的标签会导致获取数据是数据结构复杂,不建议使用

1
2
obj = pd.Series(range(5), index=['a', 'a', 'b', 'b', 'c'])
obj
a    0
a    1
b    2
b    3
c    4
dtype: int64
1
obj.index.is_unique
False
1
obj['a']
a    0
a    1
dtype: int64

汇总和计算描述统计

pandas对象拥有一组常用的数学和统计方法。它们大部分都属于约简和汇总统计,用于从Series中提取单个值(如sum或mean)或从DataFrame的行或列中提取一个Series。跟对应的NumPy数组方法相比,它们都是基于没有缺失数据的假设而构建的。

1
2
df = pd.DataFrame([[1.4, np.nan], [7.1, -4.5], [np.nan, np.nan], [0.75, -1.3]], index=['a', 'b', 'c', 'd'], columns=['one', 'two'])
df
one two
a 1.40 NaN
b 7.10 -4.5
c NaN NaN
d 0.75 -1.3
1
2
# 和 Numpy 不同的是,nan将会忽略掉继续进行计算
df.sum()
one    9.25
two   -5.80
dtype: float64
1
2
# 在 1 周方向计算
df.sum(axis=1)
a    1.40
b    2.60
c    0.00
d   -0.55
dtype: float64

NA值会自动被排除,除非整个切片(这里指的是行或列)都是NA。通过skipna选项可以禁用该功能:

1
df.sum(axis=1, skipna = False)
a     NaN
b    2.60
c     NaN
d   -0.55
dtype: float64

所有与描述统计相关的方法

相关系数与协方差

有些汇总统计(如相关系数和协方差)是通过参数对计算出来的。我们来看几个DataFrame,它们的数据来自 Yahoo!Finance 的股票价格和成交量,使用的是 pandas-datareader 包(可以用conda或pip安装):

1
conda install pandas-datareader

使用pandas_datareader模块下载了一些股票数据:

1
2
3
4
5
6
7
8
import pandas_datareader.data as web
all_data = {ticker: web.get_data_yahoo(ticker)
for ticker in ['AAPL', 'IBM', 'MSFT', 'GOOG']}
price = pd.DataFrame({ticker: data['Adj Close']
for ticker, data in all_data.items()})
volume = pd.DataFrame({ticker: data['Volume']
for ticker, data in all_data.items()})
1
2
returns = price.pct_change()
returns.tail()
AAPL IBM MSFT GOOG
Date
2019-04-11 -0.008324 0.005314 0.001165 0.002046
2019-04-12 -0.000402 0.003964 0.005152 0.010999
2019-04-15 0.001810 -0.003118 0.000827 0.002652
2019-04-16 0.000100 0.008617 -0.002313 0.004938
2019-04-17 0.019473 -0.041546 0.008280 0.007505

Series 的 corr 方法用于计算两个 Series 中重叠的、非NA的、按索引对齐的值的相关系数。与此类似, cov 用于计算协方差:

1
returns['MSFT'].corr(returns['IBM'])
0.4865732693083368
1
returns['MSFT'].cov(returns['IBM'])
8.677797505286974e-05

DataFrame 的 corr 和 cov 方法将以 DataFrame 的形式分别返回完整的相关系数或协方差矩阵:

1
returns.corr()
AAPL IBM MSFT GOOG
AAPL 1.000000 0.370756 0.452098 0.458110
IBM 0.370756 1.000000 0.486573 0.407424
MSFT 0.452098 0.486573 1.000000 0.537616
GOOG 0.458110 0.407424 0.537616 1.000000
1
returns.cov()
AAPL IBM MSFT GOOG
AAPL 0.000269 0.000075 0.000107 0.000116
IBM 0.000075 0.000152 0.000087 0.000077
MSFT 0.000107 0.000087 0.000209 0.000120
GOOG 0.000116 0.000077 0.000120 0.000236

利用DataFrame的corrwith方法,你可以计算其列或行跟另一个Series或DataFrame之间的相关系数。传入一个Series将会返回一个相关系数值Series(针对各列进行计算):

1
returns.corrwith(returns.IBM)
AAPL    0.370756
IBM     1.000000
MSFT    0.486573
GOOG    0.407424
dtype: float64

传入一个DataFrame则会计算按列名配对的相关系数。这里,我计算百分比变化与成交量的相关系数:

1
volume.head()
AAPL IBM MSFT GOOG
Date
2009-12-31 88102700.0 4223400.0 31929700.0 2455400.0
2010-01-04 123432400.0 6155300.0 38409100.0 3937800.0
2010-01-05 150476200.0 6841400.0 49749600.0 6048500.0
2010-01-06 138040000.0 5605300.0 58182400.0 8009000.0
2010-01-07 119282800.0 5840600.0 50559700.0 12912000.0
1
returns.corrwith(volume)
AAPL   -0.061663
IBM    -0.156416
MSFT   -0.089665
GOOG   -0.018289
dtype: float64
1
returns.corrwith(returns)
AAPL    1.0
IBM     1.0
MSFT    1.0
GOOG    1.0
dtype: float64

传入 axis='columns' 即可按行进行计算。无论如何,在计算相关系数之前,所有的数据项都会按标签对齐。

唯一值、值计数以及成员资格

unique 可以得到Series中的唯一值数组:

1
2
obj = pd.Series(['c', 'a', 'd', 'a', 'a', 'b', 'b', 'c', 'c'])
obj.unique()
array(['c', 'a', 'd', 'b'], dtype=object)

value_counts 用于计算一个 Series 中各值出现的频率:

1
obj.value_counts()
c    3
a    3
b    2
d    1
dtype: int64

为了便于查看,结果Series是按值频率降序排列的。value_counts还是一个顶级pandas方法,可用于任何数组或序列:

1
pd.value_counts(obj.values, sort=False)
b    2
d    1
a    3
c    3
dtype: int64

isin用于判断矢量化集合的成员资格,可用于过滤Series中或DataFrame列中数据的子集:

1
obj
0    c
1    a
2    d
3    a
4    a
5    b
6    b
7    c
8    c
dtype: object
1
2
mask = obj.isin(['b', 'c'])
mask
0     True
1    False
2    False
3    False
4    False
5     True
6     True
7     True
8     True
dtype: bool
1
obj[mask]
0    c
5    b
6    b
7    c
8    c
dtype: object

数据加载、存储与文件格式

读写文本格式的数据

pandas提供了一些用于将表格型数据读取为DataFrame对象的函数。表6-1对它们进行了总结,其中read_csv和read_table可能会是你今后用得最多的。

其中一些函数,比如pandas.read_csv,有类型推断功能,因为列数据的类型不属于数据类型。也就是说,你不需要指定列的类型到底是数值、整数、布尔值,还是字符串。其它的数据格式,如HDF5、Feather和msgpack,会在格式中存储数据类型。

读取 csv 文件

1
!cat data/examples/ex1.csv
a,b,c,d,message
1,2,3,4,hello
5,6,7,8,world
9,10,11,12,foo

由于该文件以逗号分隔,所以我们可以使用read_csv将其读入一个DataFrame:

1
2
df = pd.read_csv('data/examples/ex1.csv')
df
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo

我们还可以使用read_table,并指定分隔符:

1
pd.read_table('data/examples/ex1.csv', sep = ',')
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo

并不是所有文件都有标题行。看看下面这个文件:

1
!cat data/examples/ex2.csv
1,2,3,4,hello
5,6,7,8,world
9,10,11,12,foo

读入没有标题行的办法有两个。你可以让pandas为其分配默认的列名,也可以自己定义列名:

1
pd.read_csv('data/examples/ex2.csv', header=None)
0 1 2 3 4
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
1
pd.read_csv('data/examples/ex2.csv', names=['a', 'b', 'c', 'd', 'message'])
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo

指定某一列作为列索引

1
pd.read_csv('data/examples/ex2.csv', names=['a', 'b', 'c', 'd', 'message'], index_col='message')
a b c d
message
hello 1 2 3 4
world 5 6 7 8
foo 9 10 11 12

如果希望将多个列做成一个层次化索引,只需传入由列编号或列名组成的列表即可:

1
!cat data/examples/csv_mindex.csv
key1,key2,value1,value2
one,a,1,2
one,b,3,4
one,c,5,6
one,d,7,8
two,a,9,10
two,b,11,12
two,c,13,14
two,d,15,16
1
pd.read_csv('data/examples/csv_mindex.csv', index_col=['key1', 'key2'])
value1 value2
key1 key2
one a 1 2
b 3 4
c 5 6
d 7 8
two a 9 10
b 11 12
c 13 14
d 15 16

有些表格可能不是用固定的分隔符去分隔字段的(比如空白符或其它模式)。看看下面这个文本文件:

1
list(open('data/examples/ex3.txt'))
['            A         B         C\n',
 'aaa -0.264438 -1.026059 -0.619500\n',
 'bbb  0.927272  0.302904 -0.032399\n',
 'ccc -0.264273 -0.386314 -0.217601\n',
 'ddd -0.871858 -0.348382  1.100491\n']

虽然可以手动对数据进行规整,这里的字段是被数量不同的空白字符间隔开的。这种情况下,你可以传递一个正则表达式作为read_table的分隔符。可以用正则表达式表达为 \s+,于是有:

1
pd.read_table('data/examples/ex3.txt', sep='\s+')
A B C
aaa -0.264438 -1.026059 -0.619500
bbb 0.927272 0.302904 -0.032399
ccc -0.264273 -0.386314 -0.217601
ddd -0.871858 -0.348382 1.100491

可以用skiprows跳过文件的第一行、第三行和第四行:

1
!cat data/examples/ex4.csv
# hey!
a,b,c,d,message
# just wanted to make things more difficult for you
# who reads CSV files with computers, anyway?
1,2,3,4,hello
5,6,7,8,world
9,10,11,12,foo
1
pd.read_csv('data/examples/ex4.csv', skiprows=[0, 2, 3])
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo

缺失值处理是文件解析任务中的一个重要组成部分。缺失数据经常是要么没有(空字符串),要么用某个标记值表示。默认情况下,pandas会用一组经常出现的标记值进行识别,比如NA及NULL:

1
!cat data/examples/ex5.csv
something,a,b,c,d,message
one,1,2,3,4,NA
two,5,6,,8,world
three,9,10,11,12,foo
1
pd.read_csv('data/examples/ex5.csv')
something a b c d message
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 world
2 three 9 10 11.0 12 foo

na_values 可以用一个列表或集合的字符串表示缺失值:(把指定的值填充成 Nan)

1
2
# 把 Null 填充成空缺值
pd.read_csv('data/examples/ex5.csv', na_values=['NULL'])
something a b c d message
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 world
2 three 9 10 11.0 12 foo

字典的各列可以对不同的值用NA进行标记:

1
2
sentinels = {'message': ['foo', 'NA'], 'something': ['two']}
pd.read_csv('data/examples/ex5.csv', na_values=sentinels)
something a b c d message
0 one 1 2 3.0 4 NaN
1 NaN 5 6 NaN 8 world
2 three 9 10 11.0 12 NaN

pandas.read_csv和pandas.read_table常用的选项。


逐块读取文本文件

在处理很大的文件时,或找出大文件中的参数集以便于后续处理时,你可能只想读取文件的一小部分或逐块对文件进行迭代。

在看大文件之前,我们先设置pandas显示地更紧些:

1
2
# 最多显示 10 行
pd.options.display.max_rows = 10
1
pd.read_csv('data/examples/ex6.csv')
one two three four key
0 0.467976 -0.038649 -0.295344 -1.824726 L
1 -0.358893 1.404453 0.704965 -0.200638 B
2 -0.501840 0.659254 -0.421691 -0.057688 G
3 0.204886 1.074134 1.388361 -0.982404 R
4 0.354628 -0.133116 0.283763 -0.837063 Q
... ... ... ... ... ...
9995 2.311896 -0.417070 -1.409599 -0.515821 L
9996 -0.479893 -0.650419 0.745152 -0.646038 E
9997 0.523331 0.787112 0.486066 1.093156 K
9998 -0.362559 0.598894 -1.843201 0.887292 G
9999 -0.096376 -1.012999 -0.657431 -0.573315 0

10000 rows × 5 columns

如果只想读取几行(避免读取整个文件),通过nrows进行指定即可:

1
pd.read_csv('data/examples/ex6.csv', nrows=5)
one two three four key
0 0.467976 -0.038649 -0.295344 -1.824726 L
1 -0.358893 1.404453 0.704965 -0.200638 B
2 -0.501840 0.659254 -0.421691 -0.057688 G
3 0.204886 1.074134 1.388361 -0.982404 R
4 0.354628 -0.133116 0.283763 -0.837063 Q

要逐块读取文件,可以指定chunksize(行数):

1
2
chunker = pd.read_csv('data/examples/ex6.csv', chunksize=2)
chunker
<pandas.io.parsers.TextFileReader at 0xa1a9710>
1
2
3
4
tot = pd.Series([])
for piece in chunker:
tot = tot.add(piece['key'].value_counts(), fill_value=0)
print(piece)
           one       two     three      four key
9998 -0.362559  0.598894 -1.843201  0.887292   G
9999 -0.096376 -1.012999 -0.657431 -0.573315   0

将数据写出到文本格式

1
2
data = pd.read_csv('data/examples/ex5.csv')
data
something a b c d message
0 one 1 2 3.0 4 NaN
1 two 5 6 NaN 8 world
2 three 9 10 11.0 12 foo
1
2
# 输出到文件
data.to_csv('data/examples/out.csv')
1
!cat data/examples/out.csv
,something,a,b,c,d,message
0,one,1,2,3.0,4,
1,two,5,6,,8,world
2,three,9,10,11.0,12,foo

当然,还可以使用其他分隔符(由于这里直接写出到sys.stdout,所以仅仅是打印出文本结果而已):

1
import sys
1
2
# 设置分割符 |
data.to_csv(sys.stdout, sep='|')
|something|a|b|c|d|message
0|one|1|2|3.0|4|
1|two|5|6||8|world
2|three|9|10|11.0|12|foo

禁止输出行和列的标签(索引)

1
data.to_csv(sys.stdout, index=False, header=False)
one,1,2,3.0,4,
two,5,6,,8,world
three,9,10,11.0,12,foo

输出部分列

1
data.to_csv(sys.stdout, index=False, columns=['a', 'b', 'c'])
a,b,c
1,2,3.0
5,6,
9,10,11.0

JSON数据

1
2
3
4
5
6
7
8
9
10
11
12
obj = """
{"name": "Wes",
"places_lived": ["United States", "Spain", "Germany"],
"pet": null,
"siblings": [{"name": "Scott", "age": 30, "pets": ["Zeus", "Zuko"]},
{"name": "Katie", "age": 38,
"pets": ["Sixes", "Stache", "Cisco"]}]
}
"""
import json
result = json.loads(obj)
result
{'name': 'Wes',
 'places_lived': ['United States', 'Spain', 'Germany'],
 'pet': None,
 'siblings': [{'name': 'Scott', 'age': 30, 'pets': ['Zeus', 'Zuko']},
  {'name': 'Katie', 'age': 38, 'pets': ['Sixes', 'Stache', 'Cisco']}]}
1
2
siblings = pd.DataFrame(result['siblings'], columns=['name', 'age'])
siblings
name age
0 Scott 30
1 Katie 38

pandas.read_json可以自动将特别格式的JSON数据集转换为Series或DataFrame。例如:

1
!cat data/examples/example.json
[{"a": 1, "b": 2, "c": 3},
 {"a": 4, "b": 5, "c": 6},
 {"a": 7, "b": 8, "c": 9}]
1
2
data = pd.read_json('data/examples/example.json')
data
a b c
0 1 2 3
1 4 5 6
2 7 8 9

将数据从pandas输出到JSON,可以使用to_json方法:

1
data.to_json()
'{"a":{"0":1,"1":4,"2":7},"b":{"0":2,"1":5,"2":8},"c":{"0":3,"1":6,"2":9}}'
1
data.to_json(orient='records')
'[{"a":1,"b":2,"c":3},{"a":4,"b":5,"c":6},{"a":7,"b":8,"c":9}]'

二进制数据格式

实现数据的高效二进制格式存储最简单的办法之一是使用Python内置的pickle序列化。pandas对象都有一个用于将数据以pickle格式保存到磁盘上的to_pickle方法:

1
2
frame = pd.read_csv('data/examples/ex1.csv')
frame
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo
1
frame.to_pickle('data/examples/frame_pickle')

通过pickle直接读取被pickle化的数据,或是使用更为方便的pandas.read_pickle:

1
pd.read_pickle('data/examples/frame_pickle')
a b c d message
0 1 2 3 4 hello
1 5 6 7 8 world
2 9 10 11 12 foo

注意:pickle仅建议用于短期存储格式。其原因是很难保证该格式永远是稳定的;今天pickle的对象可能无法被后续版本的库unpickle出来。虽然我尽力保证这种事情不会发生在pandas中,但是今后的某个时候说不定还是得“打破”该pickle格式。

使用HDF5格式

HDF5是一种存储大规模科学数组数据的非常好的文件格式。它可以被作为C标准库,带有许多语言的接口,如Java、Python和MATLAB等。HDF5中的HDF指的是层次型数据格式(hierarchical data format)。每个HDF5文件都含有一个文件系统式的节点结构,它使你能够存储多个数据集并支持元数据。与其他简单格式相比,HDF5支持多种压缩器的即时压缩,还能更高效地存储重复模式数据。对于那些非常大的无法直接放入内存的数据集,HDF5就是不错的选择,因为它可以高效地分块读写。

虽然可以用PyTables或h5py库直接访问HDF5文件,pandas提供了更为高级的接口,可以简化存储Series和DataFrame对象。HDFStore类可以像字典一样,处理低级的细节:

1
2
frame = pd.DataFrame({'a': np.random.randn(100)})
frame.head()
a
0 1.863684
1 0.743116
2 -0.656781
3 0.349087
4 -0.772184
1
store = pd.HDFStore('data/examples/mydata.h5')
1
2
# 存储数据
store['obj1'] = frame
1
store['obj1_col'] = frame['a']
1
store
<class 'pandas.io.pytables.HDFStore'>
File path: data/examples/mydata.h5
1
store.close()
1
store1 = pd.HDFStore('data/examples/mydata.h5')
1
2
# 获取数据
store1['obj1'].head()
a
0 1.863684
1 0.743116
2 -0.656781
3 0.349087
4 -0.772184

读取Microsoft Excel文件

pandas的ExcelFile类或pandas.read_excel函数支持读取存储在Excel 2003(或更高版本)中的表格型数据。这两个工具分别使用扩展包xlrd和openpyxl读取XLS和XLSX文件。你可以用pip或conda安装它们。

1
2
xlsx = pd.ExcelFile('data/examples/ex1.xlsx')
pd.read_excel(xlsx, 'Sheet1')
Unnamed: 0 a b c d message
0 0 1 2 3 4 hello
1 1 5 6 7 8 world
2 2 9 10 11 12 foo

如果要读取一个文件中的多个表单,创建ExcelFile会更快,但你也可以将文件名传递到pandas.read_excel:

1
2
frame = pd.read_excel('data/examples/ex1.xlsx', 'Sheet1')
frame
Unnamed: 0 a b c d message
0 0 1 2 3 4 hello
1 1 5 6 7 8 world
2 2 9 10 11 12 foo

如果要将pandas数据写入为Excel格式,你必须首先创建一个ExcelWriter,然后使用pandas对象的to_excel方法将数据写入到其中:

1
2
3
writer = pd.ExcelWriter('data/examples/ex2.xlsx')
frame.to_excel(writer, 'Sheet1')
writer.save()

你还可以不使用ExcelWriter,而是传递文件的路径到to_excel:

1
frame.to_excel('data/examples/ex2.xlsx')

数据库交互

将数据从SQL加载到DataFrame的过程很简单,此外pandas还有一些能够简化该过程的函数。
例如,我将使用SQLite数据库(通过Python内置的sqlite3驱动器):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import sqlite3
# 创建表
query = """
CREATE TABLE test
(a VARCHAR(20), b VARCHAR(20),
c REAL, d INTEGER
);"""
con = sqlite3.connect('mydata.sqlite')
con.execute(query)
con.commit()
# 写入数据 批量写入
data = [('Atlanta', 'Georgia', 1.25, 6),
('Tallahassee', 'Florida', 2.6, 3),
('Sacramento', 'California', 1.7, 5)]
stmt = "INSERT INTO test VALUES(?, ?, ?, ?)"
con.executemany(stmt, data)
con.commit()
# 查询数据
cursor = con.execute('select * from test')
rows = cursor.fetchall()
rows
[('Atlanta', 'Georgia', 1.25, 6),
 ('Tallahassee', 'Florida', 2.6, 3),
 ('Sacramento', 'California', 1.7, 5)]
1
2
# 获取列名(字段名称)
cursor.description
(('a', None, None, None, None, None, None),
 ('b', None, None, None, None, None, None),
 ('c', None, None, None, None, None, None),
 ('d', None, None, None, None, None, None))
1
2
# 创建 dataframe
pd.DataFrame(rows, columns=[x[0] for x in cursor.description])
a b c d
0 Atlanta Georgia 1.25 6
1 Tallahassee Florida 2.60 3
2 Sacramento California 1.70 5

pandas 有一个 read_sql 函数,可以直接通过 sql 进行读取数据

1
db = sqlite3.connect('mydata.sqlite')
1
pd.read_sql('select * from test', db)
a b c d
0 Atlanta Georgia 1.25 6
1 Tallahassee Florida 2.60 3
2 Sacramento California 1.70 5

数据清洗和准备

处理缺失数据

缺失数据在pandas中呈现的方式有些不完美,但对于大多数用户可以保证功能正常。对于数值数据,pandas使用浮点值NaN(Not a Number)表示缺失数据。我们称其为哨兵值,可以方便的检测出来:

1
2
string_data = pd.Series(['aardvark', 'artichoke', np.nan, 'avocado'])
string_data
0     aardvark
1    artichoke
2          NaN
3      avocado
dtype: object
1
2
# 判断是否是空值
string_data.isnull()
0    False
1    False
2     True
3    False
dtype: bool

Python内置的None值在对象数组中也可以作为NA

1
2
string_data[0] = None
string_data
0         None
1    artichoke
2          NaN
3      avocado
dtype: object
1
string_data.isnull()
0     True
1    False
2     True
3    False
dtype: bool

一些关于缺失数据处理的函数。

滤除缺失数据

对于一个Series,dropna返回一个仅含非空数据和索引值的Series:

1
2
data = pd.Series([1, np.nan, 3.5, np.nan, 7])
data
0    1.0
1    NaN
2    3.5
3    NaN
4    7.0
dtype: float64
1
data.dropna()
0    1.0
2    3.5
4    7.0
dtype: float64
1
2
# 等价于
data[data.notnull()]
0    1.0
2    3.5
4    7.0
dtype: float64

而对于DataFrame对象,事情就有点复杂了。你可能希望丢弃全NA或含有NA的行或列。dropna默认丢弃任何含有缺失值的行:

1
2
data = pd.DataFrame([[1., 6.5, 3.], [1., NA, NA], [NA, NA, NA], [NA, 6.5, 3.]])
data
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0
1
data.dropna()
0 1 2
0 1.0 6.5 3.0

传入 how='all' 将只丢弃全为NA的那些行:

1
data.dropna(how='all')
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
3 NaN 6.5 3.0

丢弃列,只需传入axis=1即可:

1
2
data[4] = NA
data
0 1 2 4
0 1.0 6.5 3.0 NaN
1 1.0 NaN NaN NaN
2 NaN NaN NaN NaN
3 NaN 6.5 3.0 NaN
1
data.dropna(axis=1, how='all')
0 1 2
0 1.0 6.5 3.0
1 1.0 NaN NaN
2 NaN NaN NaN
3 NaN 6.5 3.0

thresh 参数 删除 行、列 空值超过指定数字的

1
data
0 1 2 4
0 1.0 6.5 3.0 NaN
1 1.0 NaN NaN NaN
2 NaN NaN NaN NaN
3 NaN 6.5 3.0 NaN
1
2
# 删除 行 方向 nan 大于 2 的行
data.dropna(thresh=2)
0 1 2 4
0 1.0 6.5 3.0 NaN
3 NaN 6.5 3.0 NaN

填充缺失数据

对于大多数情况而言,fillna方法是最主要的函数。通过一个常数调用fillna就会将缺失值替换为那个常数值:

1
2
3
4
df = pd.DataFrame(np.random.randn(7, 3))
df.iloc[:4, 1] = NA
df.iloc[:2, 2] = NA
df
0 1 2
0 0.187550 NaN NaN
1 -0.903578 NaN NaN
2 0.142988 NaN -1.629844
3 0.346151 NaN -0.806237
4 0.125914 -0.188009 -1.067930
5 -1.181948 0.289074 -0.852676
6 0.568097 -0.950069 -2.165337
1
2
# 填充成 0
df.fillna(0)
0 1 2
0 0.187550 0.000000 0.000000
1 -0.903578 0.000000 0.000000
2 0.142988 0.000000 -1.629844
3 0.346151 0.000000 -0.806237
4 0.125914 -0.188009 -1.067930
5 -1.181948 0.289074 -0.852676
6 0.568097 -0.950069 -2.165337

若是通过一个字典调用fillna,就可以实现对不同的列填充不同的值:

1
2
# 1 列填充 0.5 ;2 列填充 0
df.fillna({1: 0.5, 2: 0})
0 1 2
0 0.187550 0.500000 0.000000
1 -0.903578 0.500000 0.000000
2 0.142988 0.500000 -1.629844
3 0.346151 0.500000 -0.806237
4 0.125914 -0.188009 -1.067930
5 -1.181948 0.289074 -0.852676
6 0.568097 -0.950069 -2.165337

fillna默认会返回新对象,但也可以对现有对象进行就地修改:

1
2
df.fillna(0, inplace=True)
df
0 1 2
0 0.187550 0.000000 0.000000
1 -0.903578 0.000000 0.000000
2 0.142988 0.000000 -1.629844
3 0.346151 0.000000 -0.806237
4 0.125914 -0.188009 -1.067930
5 -1.181948 0.289074 -0.852676
6 0.568097 -0.950069 -2.165337

对 reindex 有效的那些插值方法也可用于fillna:

1
2
3
4
df = pd.DataFrame(np.random.randn(6, 3))
df.iloc[2:, 1] = NA
df.iloc[4:, 2] = NA
df
0 1 2
0 -2.190504 -1.963825 -1.545763
1 0.306294 -1.290580 -1.274680
2 0.535840 NaN 0.219375
3 0.938590 NaN -1.277384
4 0.747602 NaN NaN
5 -0.113758 NaN NaN
1
2
# ffill 使用 前值填充
df.fillna(method='ffill')
0 1 2
0 -2.190504 -1.963825 -1.545763
1 0.306294 -1.290580 -1.274680
2 0.535840 -1.290580 0.219375
3 0.938590 -1.290580 -1.277384
4 0.747602 -1.290580 -1.277384
5 -0.113758 -1.290580 -1.277384
1
2
# limit 限制填充的条数
df.fillna(method='ffill', limit=2)
0 1 2
0 -2.190504 -1.963825 -1.545763
1 0.306294 -1.290580 -1.274680
2 0.535840 -1.290580 0.219375
3 0.938590 -1.290580 -1.277384
4 0.747602 NaN -1.277384
5 -0.113758 NaN -1.277384

可以传入Series的平均值或中位数:

1
2
data = pd.Series([1., NA, 3.5, NA, 7])
data
0    1.0
1    NaN
2    3.5
3    NaN
4    7.0
dtype: float64
1
data.fillna(data.mean())
0    1.000000
1    3.833333
2    3.500000
3    3.833333
4    7.000000
dtype: float64

fillna函数参数

数据转换

移除重复数据

1
2
data = pd.DataFrame({'k1': ['one', 'two'] * 3 + ['two'], 'k2': [1, 1, 2, 3, 3, 4, 4]})
data
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4
6 two 4

duplicated 方法返回一个布尔型 Series,表示各行是否是重复行(前面出现过的行):

1
data.duplicated()
0    False
1    False
2    False
3    False
4    False
5    False
6     True
dtype: bool

drop_duplicates 方法,它会返回一个DataFrame,重复的数据会被删除

1
data.drop_duplicates()
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
5 two 4

这两个方法默认会判断全部列,你也可以指定部分列进行重复项判断。

1
data.drop_duplicates(['k1'])
k1 k2
0 one 1
1 two 1

duplicated 和 drop_duplicates 默认保留的是第一个出现的值组合。传入 keep='last' 则保留最后一个:

1
data.drop_duplicates(['k1', 'k2'], keep='last')
k1 k2
0 one 1
1 two 1
2 one 2
3 two 3
4 one 3
6 two 4

利用函数或映射进行数据转换

利用函数或映射进行数据转换

我们来看看下面这组有关肉类的数据:

1
2
3
data = pd.DataFrame({'food': ['bacon', 'pulled pork', 'bacon', 'Pastrami', 'corned beef', 'Bacon','pastrami', 'honey ham', 'nova lox'],
'ounces': [4, 3, 12, 6, 7.5, 8, 3, 5, 6]})
data
food ounces
0 bacon 4.0
1 pulled pork 3.0
2 bacon 12.0
3 Pastrami 6.0
4 corned beef 7.5
5 Bacon 8.0
6 pastrami 3.0
7 honey ham 5.0
8 nova lox 6.0

假设你想要添加一列表示该肉类食物来源的动物类型。我们先编写一个不同肉类到动物的映射:

1
2
3
4
5
6
7
8
meat_to_animal = {
'bacon': 'pig',
'pulled pork': 'pig',
'pastrami': 'cow',
'corned beef': 'cow',
'honey ham': 'pig',
'nova lox': 'salmon'
}

列字符转换成小写

1
2
lowercased = data['food'].str.lower()
lowercased
0          bacon
1    pulled pork
2          bacon
3       pastrami
4    corned beef
5          bacon
6       pastrami
7      honey ham
8       nova lox
Name: food, dtype: object
1
lowercased.map(meat_to_animal)
0       pig
1       pig
2       pig
3       cow
4       cow
5       pig
6       cow
7       pig
8    salmon
Name: food, dtype: object
1
2
data['animal'] = lowercased.map(meat_to_animal)
data
food ounces animal
0 bacon 4.0 pig
1 pulled pork 3.0 pig
2 bacon 12.0 pig
3 Pastrami 6.0 cow
4 corned beef 7.5 cow
5 Bacon 8.0 pig
6 pastrami 3.0 cow
7 honey ham 5.0 pig
8 nova lox 6.0 salmon

我们也可以使用下面的这种方式

1
data['food'].map(lambda x: meat_to_animal[x.lower()])
0       pig
1       pig
2       pig
3       cow
4       cow
5       pig
6       cow
7       pig
8    salmon
Name: food, dtype: object

替换值

1
2
data = pd.Series([1., -999., 2., -999., -1000., 3.])
data
0       1.0
1    -999.0
2       2.0
3    -999.0
4   -1000.0
5       3.0
dtype: float64

-999这个值可能是一个表示缺失数据的标记值。要将其替换为pandas能够理解的NA值,我们可以利用replace来产生一个新的Series(除非传入inplace=True):

1
data.replace(-999, np.nan)
0       1.0
1       NaN
2       2.0
3       NaN
4   -1000.0
5       3.0
dtype: float64

如果你希望一次性替换多个值,可以传入一个由待替换值组成的列表以及一个替换值:

1
data.replace([-999, -1000], np.nan)
0    1.0
1    NaN
2    2.0
3    NaN
4    NaN
5    3.0
dtype: float64

要让每个值有不同的替换值,可以传递一个替换列表:

1
data.replace([-999, -1000], [np.nan, 0])
0    1.0
1    NaN
2    2.0
3    NaN
4    0.0
5    3.0
dtype: float64

传入的参数也可以是字典:

1
data.replace({-999: np.nan, -1000: 0})
0    1.0
1    NaN
2    2.0
3    NaN
4    0.0
5    3.0
dtype: float64

重命名轴索引

1
2
data = pd.DataFrame(np.arange(12).reshape((3, 4)), index=['Ohio', 'Colorado', 'New York'], columns=['one', 'two', 'three', 'four'])
data
one two three four
Ohio 0 1 2 3
Colorado 4 5 6 7
New York 8 9 10 11

跟Series一样,轴索引也有一个map方法:

1
2
transform = lambda x: x[:4].upper()
data.index.map(transform)
Index(['OHIO', 'COLO', 'NEW '], dtype='object')

将其赋值给index,这样就可以对DataFrame进行就地修改:

1
2
data.index = data.index.map(transform)
data
one two three four
OHIO 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11

如果想要创建数据集的转换版(而不是修改原始数据),比较实用的方法是rename:

1
data.rename(index=str.title, columns=str.upper)
ONE TWO THREE FOUR
Ohio 0 1 2 3
Colo 4 5 6 7
New 8 9 10 11

rename可以结合字典型对象实现对部分轴标签的更新:

1
data.rename(index={'OHIO': 'INDIANA'}, columns={'three': 'peekaboo'})
one two peekaboo four
INDIANA 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11

rename可以实现复制DataFrame并对其索引和列标签进行赋值。如果希望就地修改某个数据集,传入 inplace=True 即可:

1
2
data.rename(index={'OHIO': 'INDIANA'}, inplace=True)
data
one two three four
INDIANA 0 1 2 3
COLO 4 5 6 7
NEW 8 9 10 11

离散化和面元划分

为了便于分析,连续数据常常被离散化或拆分为“面元”(bin)。假设有一组人员数据,而你希望将它们划分为不同的年龄组:

1
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32]

接下来将这些数据划分为“18到25”、“26到35”、“35到60”以及“60以上”几个面元。要实现该功能,你需要使用pandas的cut函数:

1
2
3
bins = [18, 25, 35, 60, 100]
cats = pd.cut(ages, bins)
cats
[(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]]
Length: 12
Categories (4, interval[int64]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]]

pandas返回的是一个特殊的Categorical对象。结果展示了pandas.cut划分的面元。你可以将其看做一组表示面元名称的字符串。它的底层含有一个表示不同分类名称的类型数组,以及一个codes属性中的年龄数据的标签:

1
2
# 统计值所在的元面
cats.codes
array([0, 0, 0, 1, 0, 0, 2, 1, 3, 2, 2, 1], dtype=int8)
1
2
# 获得分组
cats.categories
IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]]
              closed='right',
              dtype='interval[int64]')
1
2
# pd.value_counts(cats)是pandas.cut结果的面元计数。
pd.value_counts(cats)
(18, 25]     5
(35, 60]     3
(25, 35]     3
(60, 100]    1
dtype: int64

跟“区间”的数学符号一样,圆括号表示开端,而方括号则表示闭端(包括)。哪边是闭端可以通过right=False进行修改:

1
pd.cut(ages, [18, 26, 36, 61, 100], right=False)
[[18, 26), [18, 26), [18, 26), [26, 36), [18, 26), ..., [26, 36), [61, 100), [36, 61), [36, 61), [26, 36)]
Length: 12
Categories (4, interval[int64]): [[18, 26) < [26, 36) < [36, 61) < [61, 100)]

通过传递一个列表或数组到labels,设置自己的面元名称:

1
2
group_names = ['Youth', 'YoungAdult', 'MiddleAged', 'Senior']
pd.cut(ages, bins, labels=group_names)
[Youth, Youth, Youth, YoungAdult, Youth, ..., YoungAdult, Senior, MiddleAged, MiddleAged, YoungAdult]
Length: 12
Categories (4, object): [Youth < YoungAdult < MiddleAged < Senior]

如果向cut传入的是面元的数量而不是确切的面元边界,则它会根据数据的最小值和最大值计算等长面元。

1
2
3
data = np.random.rand(20)
# 等分成 4 份, 选项precision=2,限定小数只有两位。
pd.cut(data, 4, precision=2)
[(0.49, 0.72], (0.015, 0.25], (0.49, 0.72], (0.49, 0.72], (0.25, 0.49], ..., (0.25, 0.49], (0.25, 0.49], (0.72, 0.96], (0.015, 0.25], (0.25, 0.49]]
Length: 20
Categories (4, interval[float64]): [(0.015, 0.25] < (0.25, 0.49] < (0.49, 0.72] < (0.72, 0.96]]

qcut是一个非常类似于cut的函数,它可以根据样本分位数对数据进行面元划分。根据数据的分布情况,cut可能无法使各个面元中含有相同数量的数据点。而qcut由于使用的是样本分位数,因此可以得到大小基本相等的面元:

1
2
3
data = np.random.randn(1000)
cats = pd.qcut(data, 4)
cats
[(-0.694, -0.0168], (-3.589, -0.694], (0.638, 3.369], (-0.0168, 0.638], (-3.589, -0.694], ..., (-0.694, -0.0168], (-3.589, -0.694], (-0.694, -0.0168], (-3.589, -0.694], (-0.0168, 0.638]]
Length: 1000
Categories (4, interval[float64]): [(-3.589, -0.694] < (-0.694, -0.0168] < (-0.0168, 0.638] < (0.638, 3.369]]
1
pd.value_counts(cats)
(0.638, 3.369]       250
(-0.0168, 0.638]     250
(-0.694, -0.0168]    250
(-3.589, -0.694]     250
dtype: int64

检测和过滤异常值

过滤或变换异常值在很大程度上就是运用数组运算。来看一个含有正态分布数据的DataFrame:

1
2
data = pd.DataFrame(np.random.randn(1000, 4))
data.describe()
0 1 2 3
count 1000.000000 1000.000000 1000.000000 1000.000000
mean -0.064053 -0.005447 0.038026 0.024811
std 0.999489 0.948043 0.953395 1.021102
min -3.105504 -2.844008 -2.760611 -4.492430
25% -0.718899 -0.656214 -0.597055 -0.646600
50% -0.020346 -0.019434 0.058547 -0.003166
75% 0.578055 0.649719 0.661221 0.690373
max 3.106261 2.717680 3.289203 3.243883

假设你想要找出某列中绝对值大小超过3的值

1
2
col = data[2]
col[np.abs(col) > 3]
308    3.289203
Name: 2, dtype: float64

要选出全部含有“超过3或-3的值”的行,你可以在布尔型DataFrame中使用any方法:

1
data[(np.abs(data) > 3).any(1)]
0 1 2 3
8 -3.105504 -0.094313 -0.186817 0.461499
96 0.522268 -0.201284 0.145961 3.151065
121 3.106261 -2.029135 -0.179609 -0.478008
148 0.019824 0.115120 1.005103 -4.492430
308 0.770344 -0.635648 3.289203 -0.151866
434 -0.594539 -0.124445 0.535540 3.243883
454 0.196540 0.002230 1.150842 -3.098229
475 1.327124 0.532669 -0.285367 3.095290
604 1.154687 0.336886 -0.767514 3.229107
659 -3.076200 -0.979556 0.653771 0.559905

排列和随机采样

利用 numpy.random.permutation 函数可以轻松实现对Series或DataFrame的列的排列工作(permuting,随机重排序)。通过需要排列的轴的长度调用 permutation,可产生一个表示新顺序的整数数组:

1
2
df = pd.DataFrame(np.arange(5 * 4).reshape((5, 4)))
df
0 1 2 3
0 0 1 2 3
1 4 5 6 7
2 8 9 10 11
3 12 13 14 15
4 16 17 18 19
1
2
sampler = np.random.permutation(5)
sampler
array([2, 1, 4, 3, 0])

然后就可以在基于iloc的索引操作或take函数中使用该数组了:

1
df.take(sampler)
0 1 2 3
2 8 9 10 11
1 4 5 6 7
4 16 17 18 19
3 12 13 14 15
0 0 1 2 3

选取随机子集(不重复的)

1
df.sample(n=3)
0 1 2 3
3 12 13 14 15
1 4 5 6 7
2 8 9 10 11

选取随机子集(允许重复),不能操作 DateFrame

1
2
choices = pd.Series([5, 7, -1, 6, 4])
choices.sample(n=10, replace=True)
0    5
4    4
0    5
0    5
2   -1
3    6
3    6
3    6
3    6
0    5
dtype: int64
1
choices
0    5
1    7
2   -1
3    6
4    4
dtype: int64

计算指标/哑变量

另一种常用于统计建模或机器学习的转换方式是:将分类变量(categorical variable)转换为“哑变量”或“指标矩阵”。

如果DataFrame的某一列中含有k个不同的值,则可以派生出一个k列矩阵或DataFrame(其值全为1和0)。pandas有一个get_dummies函数可以实现该功能(其实自己动手做一个也不难)。使用之前的一个DataFrame例子:

1
2
df = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'b'], 'data1': range(6)})
df
key data1
0 b 0
1 b 1
2 a 2
3 c 3
4 a 4
5 b 5
1
pd.get_dummies(df['key'])
a b c
0 0 1 0
1 0 1 0
2 1 0 0
3 0 0 1
4 1 0 0
5 0 1 0

有时候,你可能想给指标DataFrame的列加上一个前缀,以便能够跟其他数据进行合并。get_dummies的prefix参数可以实现该功能:

1
2
dummies = pd.get_dummies(df['key'], prefix='key')
dummies
key_a key_b key_c
0 0 1 0
1 0 1 0
2 1 0 0
3 0 0 1
4 1 0 0
5 0 1 0
1
2
3
# 拼接
df_with_dummy = df[['data1']].join(dummies)
df_with_dummy
data1 key_a key_b key_c
0 0 0 1 0
1 1 0 1 0
2 2 1 0 0
3 3 0 0 1
4 4 1 0 0
5 5 0 1 0

如果DataFrame中的某行同属于多个分类,则事情就会有点复杂。看一下MovieLens 1M数据集

1
2
3
mnames = ['movie_id', 'title', 'genres']
movies = pd.read_table('data/movielens/movies.dat', sep='::', header=None, names=mnames)
movies.head()
C:\ProgramData\Anaconda3\lib\site-packages\ipykernel_launcher.py:2: ParserWarning: Falling back to the 'python' engine because the 'c' engine does not support regex separators (separators > 1 char and different from '\s+' are interpreted as regex); you can avoid this warning by specifying engine='python'.
movie_id title genres
0 1 Toy Story (1995) Animation|Children's|Comedy
1 2 Jumanji (1995) Adventure|Children's|Fantasy
2 3 Grumpier Old Men (1995) Comedy|Romance
3 4 Waiting to Exhale (1995) Comedy|Drama
4 5 Father of the Bride Part II (1995) Comedy

要为每个genre添加指标变量就需要做一些数据规整操作。首先,我们从数据集中抽取出不同的genre值:

1
2
3
4
5
6
7
all_genres = []
for x in movies.genres:
all_genres.extend(x.split('|'))
genres = pd.unique(all_genres)
genres
array(['Animation', "Children's", 'Comedy', 'Adventure', 'Fantasy',
       'Romance', 'Drama', 'Action', 'Crime', 'Thriller', 'Horror',
       'Sci-Fi', 'Documentary', 'War', 'Musical', 'Mystery', 'Film-Noir',
       'Western'], dtype=object)

构建指标DataFrame的方法之一是从一个全零DataFrame开始:

1
2
zero_matrix = np.zeros((len(movies), len(genres)))
zero_matrix
array([[0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       ...,
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 0.]])
1
2
dummies = pd.DataFrame(zero_matrix, columns=genres)
dummies.head()
Animation Children's Comedy Adventure Fantasy Romance Drama Action Crime Thriller Horror Sci-Fi Documentary War Musical Mystery Film-Noir Western
0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
1 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

现在,迭代每一部电影,并将dummies各行的条目设为1。要这么做,我们使用dummies.columns来计算每个类型的列索引:

1
2
gen = movies.genres[0]
gen
"Animation|Children's|Comedy"
1
gen.split('|')
['Animation', "Children's", 'Comedy']
1
2
# 获取值对应的下标
dummies.columns.get_indexer(gen.split('|'))
array([0, 1, 2], dtype=int64)

根据索引,使用.iloc设定值:

1
2
3
for i, gen in enumerate(movies.genres):
indices = dummies.columns.get_indexer(gen.split('|'))
dummies.iloc[i, indices] = 1

将其与movies合并起来:

1
2
movies_windic = movies.join(dummies.add_prefix('Genre_'))
movies_windic.head()
movie_id title genres Genre_Animation Genre_Children's Genre_Comedy Genre_Adventure Genre_Fantasy Genre_Romance Genre_Drama ... Genre_Crime Genre_Thriller Genre_Horror Genre_Sci-Fi Genre_Documentary Genre_War Genre_Musical Genre_Mystery Genre_Film-Noir Genre_Western
0 1 Toy Story (1995) Animation|Children's|Comedy 1.0 1.0 1.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
1 2 Jumanji (1995) Adventure|Children's|Fantasy 0.0 1.0 0.0 1.0 1.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
2 3 Grumpier Old Men (1995) Comedy|Romance 0.0 0.0 1.0 0.0 0.0 1.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
3 4 Waiting to Exhale (1995) Comedy|Drama 0.0 0.0 1.0 0.0 0.0 0.0 1.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
4 5 Father of the Bride Part II (1995) Comedy 0.0 0.0 1.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

5 rows × 21 columns

字符串操作

pandas 的矢量化字符串函数

1
2
3
data = {'Dave': 'dave@google.com', 'Steve': 'steve@gmail.com', 'Rob': 'rob@gmail.com', 'Wes': np.nan}
data = pd.Series(data)
data
Dave     dave@google.com
Steve    steve@gmail.com
Rob        rob@gmail.com
Wes                  NaN
dtype: object
1
data.isnull()
Dave     False
Steve    False
Rob      False
Wes       True
dtype: bool

通过 data.map,所有字符串和正则表达式方法都能被应用于(传入lambda表达式或其他函数)各个值,但是如果存在 NA(null) 就会报错。为了解决这个问题,Series有一些能够跳过NA值的面向数组方法,进行字符串操作。通过Series的 str 属性即可访问这些方法。例如,我们可以通过 str.contains 检查各个电子邮件地址是否含有”gmail”:

1
data.str.contains('gmail')
Dave     False
Steve     True
Rob       True
Wes        NaN
dtype: object

也可以使用正则表达式,还可以加上任意re选项(如IGNORECASE):

1
2
3
pattern = '([A-Z0-9._%+-]+)@([A-Z0-9.-]+)\\.([A-Z]{2,4})'
temp = data.str.findall(pattern, flags=re.IGNORECASE)
temp
Dave     [(dave, google, com)]
Steve    [(steve, gmail, com)]
Rob        [(rob, gmail, com)]
Wes                        NaN
dtype: object

对字符串进行截取

1
data.str[:5]
Dave     dave@
Steve    steve
Rob      rob@g
Wes        NaN
dtype: object

更多的pandas字符串方法

数据规整:聚合、合并和重塑

层次化索引

层次化索引(hierarchical indexing)是pandas的一项重要功能,它使你能在一个轴上拥有多个(两个以上)索引级别。

1
2
data = pd.Series(np.random.randn(9), index=[['a', 'a', 'a', 'b', 'b', 'c', 'c', 'd', 'd'], [1, 2, 3, 1, 3, 1, 2, 2, 3]])
data
a  1    1.280879
   2   -0.233278
   3    0.700301
b  1    0.115678
   3    0.390445
c  1   -0.816532
   2   -0.972933
d  2    2.053691
   3    1.166844
dtype: float64
1
data.index
MultiIndex(levels=[['a', 'b', 'c', 'd'], [1, 2, 3]],
           labels=[[0, 0, 0, 1, 1, 2, 2, 3, 3], [0, 1, 2, 0, 2, 0, 1, 1, 2]])

对于一个层次化索引的对象,可以使用所谓的部分索引,使用它选取数据子集的操作更简单:

1
data['b']
1    0.115678
3    0.390445
dtype: float64
1
data['b':'c']
b  1    0.115678
   3    0.390445
c  1   -0.816532
   2   -0.972933
dtype: float64
1
data[['b','c']]
b  1    0.115678
   3    0.390445
c  1   -0.816532
   2   -0.972933
dtype: float64

在“内层”中进行选取

1
data.loc[:, 2]
a   -0.233278
c   -0.972933
d    2.053691
dtype: float64

unstack方法将这段数据重新安排到一个DataFrame中:

1
data.unstack()
1 2 3
a 1.280879 -0.233278 0.700301
b 0.115678 NaN 0.390445
c -0.816532 -0.972933 NaN
d NaN 2.053691 1.166844

unstack的逆运算是stack:

1
data.unstack().stack()
a  1    1.280879
   2   -0.233278
   3    0.700301
b  1    0.115678
   3    0.390445
c  1   -0.816532
   2   -0.972933
d  2    2.053691
   3    1.166844
dtype: float64

对于一个DataFrame,每条轴都可以有分层索引:

1
2
3
frame = pd.DataFrame(np.arange(12).reshape((4, 3)), index=[['a', 'a', 'b', 'b'], [1, 2, 1, 2]],
columns=[['Ohio', 'Ohio', 'Colorado'], ['Green', 'Red', 'Green']])
frame
Ohio Colorado
Green Red Green
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11

各层都可以有名字(可以是字符串,也可以是别的Python对象)。如果指定了名称,它们就会显示在控制台输出中:

1
2
3
frame.index.names = ['key1', 'key2']
frame.columns.names = ['state', 'color']
frame
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11

有了部分列索引,因此可以轻松选取列分组:

1
frame['Ohio']
color Green Red
key1 key2
a 1 0 1
2 3 4
b 1 6 7
2 9 10
1
frame.index
MultiIndex(levels=[['a', 'b'], [1, 2]],
           codes=[[0, 0, 1, 1], [0, 1, 0, 1]],
           names=['key1', 'key2'])

重排与分级排序

重新调整某条轴上各级别的顺序,或根据指定级别上的值对数据进行排序。

swaplevel接受两个级别编号或名称,并返回一个互换了级别的新对象(但数据不会发生变化):

1
frame
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11
1
2
# 交换 key1 和 key2 位置
frame.swaplevel('key1', 'key2')
state Ohio Colorado
color Green Red Green
key2 key1
1 a 0 1 2
2 a 3 4 5
1 b 6 7 8
2 b 9 10 11

sort_index则根据单个级别中的值对数据进行排序。

1
2
# 这里 level = 1 指的是 key2 列
frame.sort_index(level=1)
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
b 1 6 7 8
a 2 3 4 5
b 2 9 10 11
1
frame.sort_index(level=0)
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11
1
frame.swaplevel(0, 1).sort_index(level=0)
state Ohio Colorado
color Green Red Green
key2 key1
1 a 0 1 2
b 6 7 8
2 a 3 4 5
b 9 10 11

根据级别汇总统计

许多对DataFrame和Series的描述和汇总统计都有一个level选项,它用于指定在某条轴上求和的级别。

1
frame
state Ohio Colorado
color Green Red Green
key1 key2
a 1 0 1 2
2 3 4 5
b 1 6 7 8
2 9 10 11
1
2
# 列方向 根据 level = key2 求和
frame.sum(level='key2')
state Ohio Colorado
color Green Red Green
key2
1 6 8 10
2 12 14 16
1
2
# 行方向 更具 level = color 求和
frame.sum(level='color', axis=1)
color Green Red
key1 key2
a 1 2 1
2 8 4
b 1 14 7
2 20 10

使用DataFrame的列进行索引

人们经常想要将DataFrame的一个或多个列当做行索引来用,或者可能希望将行索引变成DataFrame的列。
DataFrame的set_index函数会将其一个或多个列转换为行索引,并创建一个新的DataFrame:

1
2
3
4
frame = pd.DataFrame({'a': range(7), 'b': range(7, 0, -1),
'c': ['one', 'one', 'one', 'two', 'two', 'two', 'two'],
'd': [0, 1, 2, 0, 1, 2, 3]})
frame
a b c d
0 0 7 one 0
1 1 6 one 1
2 2 5 one 2
3 3 4 two 0
4 4 3 two 1
5 5 2 two 2
6 6 1 two 3
1
2
frame2 = frame.set_index(['c', 'd'])
frame2
a b
c d
one 0 0 7
1 1 6
2 2 5
two 0 3 4
1 4 3
2 5 2
3 6 1

默认情况下,那些列会从DataFrame中移除,但也可以将其保留下来:

1
frame.set_index(['c', 'd'], drop=False)
a b c d
c d
one 0 0 7 one 0
1 1 6 one 1
2 2 5 one 2
two 0 3 4 two 0
1 4 3 two 1
2 5 2 two 2
3 6 1 two 3

reset_index的功能跟set_index刚好相反,层次化索引的级别会被转移到列里面:

1
frame2.reset_index()
c d a b
0 one 0 0 7
1 one 1 1 6
2 one 2 2 5
3 two 0 3 4
4 two 1 4 3
5 two 2 5 2
6 two 3 6 1

合并数据集

pandas对象中的数据可以通过一些方式进行合并:

  1. pandas.merge可根据一个或多个键将不同DataFrame中的行连接起来。SQL或其他关系型数据库的用户对此应该会比较熟悉,因为它实现的就是数据库的join操作。
  2. pandas.concat可以沿着一条轴将多个对象堆叠到一起。
  3. 实例方法combine_first可以将重复数据拼接在一起,用一个对象中的值填充另一个对象中的缺失值。

数据库风格的DataFrame合并

数据集的合并(merge)或连接(join)运算是通过一个或多个键将行连接起来的。这些运算是关系型数据库(基于SQL)的核心。pandas的merge函数是对数据应用这些算法的主要切入点。

1
2
3
4
5
df1 = pd.DataFrame({'key': ['b', 'b', 'a', 'c', 'a', 'a', 'b'], 'data1': range(7)})
df2 = pd.DataFrame({'key': ['a', 'b', 'd'], 'data2': range(3)})
df1
key data1
0 b 0
1 b 1
2 a 2
3 c 3
4 a 4
5 a 5
6 b 6
1
df2
key data2
0 a 0
1 b 1
2 d 2

如果没有指定关联的列,merge就会将重叠列的列名当做键。默认情况下,merge做的是“内连接”;结果中的键是交集。

1
pd.merge(df1, df2)
key data1 data2
0 b 0 1
1 b 1 1
2 b 6 1
3 a 2 0
4 a 4 0
5 a 5 0

指明要用哪个列进行连接。

1
pd.merge(df1, df2, on='key')
key data1 data2
0 b 0 1
1 b 1 1
2 b 6 1
3 a 2 0
4 a 4 0
5 a 5 0

如果两个对象的列名不同,也可以分别进行指定:

1
2
3
4
5
df3 = pd.DataFrame({'lkey': ['b', 'b', 'a', 'c', 'a', 'a', 'b'], 'data1': range(7)})
df4 = pd.DataFrame({'rkey': ['a', 'b', 'd'], 'data2': range(3)})
pd.merge(df3, df4, left_on='lkey', right_on='rkey')
lkey data1 rkey data2
0 b 0 b 1
1 b 1 b 1
2 b 6 b 1
3 a 2 a 0
4 a 4 a 0
5 a 5 a 0

指定连接方式

1
pd.merge(df1, df2, how='outer')
key data1 data2
0 b 0.0 1.0
1 b 1.0 1.0
2 b 6.0 1.0
3 a 2.0 0.0
4 a 4.0 0.0
5 a 5.0 0.0
6 c 3.0 NaN
7 d NaN 2.0

不同的连接类型

要根据多个键进行合并,传入一个由列名组成的列表即可:

1
2
3
4
5
6
7
8
left = pd.DataFrame({'key1': ['foo', 'foo', 'bar'],
'key2': ['one', 'two', 'one'],
'lval': [1, 2, 3]})
right = pd.DataFrame({'key1': ['foo', 'foo', 'bar', 'bar'],
'key2': ['one', 'one', 'one', 'two'],
'rval': [4, 5, 6, 7]})
left
key1 key2 lval
0 foo one 1
1 foo two 2
2 bar one 3
1
right
key1 key2 rval
0 foo one 4
1 foo one 5
2 bar one 6
3 bar two 7
1
pd.merge(left, right, on=['key1', 'key2'], how='outer')
key1 key2 lval rval
0 foo one 1.0 4.0
1 foo one 1.0 5.0
2 foo two 2.0 NaN
3 bar one 3.0 6.0
4 bar two NaN 7.0

对于合并运算需要考虑的最后一个问题是对重复列名的处理。虽然你可以手工处理列名重叠的问题(查看前面介绍的重命名轴标签),但merge有一个更实用的suffixes选项,用于指定附加到左右两个DataFrame对象的重叠列名上的字符串:

1
pd.merge(left, right, on='key1')
key1 key2_x lval key2_y rval
0 foo one 1 one 4
1 foo one 1 one 5
2 foo two 2 one 4
3 foo two 2 one 5
4 bar one 3 one 6
5 bar one 3 two 7
1
pd.merge(left, right, on='key1', suffixes=('_left', '_right'))
key1 key2_left lval key2_right rval
0 foo one 1 one 4
1 foo one 1 one 5
2 foo two 2 one 4
3 foo two 2 one 5
4 bar one 3 one 6
5 bar one 3 two 7

merge函数的参数

1

2

索引上的合并

有时候,DataFrame中的连接键位于其索引中。在这种情况下,你可以传入 left_index=Trueright_index=True(或两个都传)以说明索引应该被用作连接键:

1
2
left1 = pd.DataFrame({'key': ['a', 'b', 'a', 'a', 'b', 'c'], 'value': range(6)})
left1
key value
0 a 0
1 b 1
2 a 2
3 a 3
4 b 4
5 c 5
1
2
right1 = pd.DataFrame({'group_val': [3.5, 7]}, index=['a', 'b'])
right1
group_val
a 3.5
b 7.0
1
pd.merge(left1, right1, left_on='key', right_index=True)
key value group_val
0 a 0 3.5
2 a 2 3.5
3 a 3 3.5
1 b 1 7.0
4 b 4 7.0

对于层次化索引的数据,事情就有点复杂了,因为索引的合并默认是多键合并:

1
2
3
4
lefth = pd.DataFrame({'key1': ['Ohio', 'Ohio', 'Ohio', 'Nevada', 'Nevada'],
'key2': [2000, 2001, 2002, 2001, 2002],
'data': np.arange(5.)})
lefth
key1 key2 data
0 Ohio 2000 0.0
1 Ohio 2001 1.0
2 Ohio 2002 2.0
3 Nevada 2001 3.0
4 Nevada 2002 4.0
1
2
3
4
5
righth = pd.DataFrame(np.arange(12).reshape((6, 2)),
index=[['Nevada', 'Nevada', 'Ohio', 'Ohio', 'Ohio', 'Ohio'],
[2001, 2000, 2000, 2000, 2001, 2002]],
columns=['event1', 'event2'])
righth
event1 event2
Nevada 2001 0 1
2000 2 3
Ohio 2000 4 5
2000 6 7
2001 8 9
2002 10 11

这种情况下,你必须以列表的形式指明用作合并键的多个列

1
pd.merge(lefth, righth, left_on=['key1', 'key2'], right_index=True)
key1 key2 data event1 event2
0 Ohio 2000 0.0 4 5
0 Ohio 2000 0.0 6 7
1 Ohio 2001 1.0 8 9
2 Ohio 2002 2.0 10 11
3 Nevada 2001 3.0 0 1

同时使用合并双方的索引也没问题:

1
2
3
4
left2 = pd.DataFrame([[1., 2.], [3., 4.], [5., 6.]],
index=['a', 'c', 'e'],
columns=['Ohio', 'Nevada'])
left2
Ohio Nevada
a 1.0 2.0
c 3.0 4.0
e 5.0 6.0
1
2
3
4
right2 = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.], [13, 14]],
index=['b', 'c', 'd', 'e'],
columns=['Missouri', 'Alabama'])
right2
Missouri Alabama
b 7.0 8.0
c 9.0 10.0
d 11.0 12.0
e 13.0 14.0
1
pd.merge(left2, right2, how='outer', left_index=True, right_index=True)
Ohio Nevada Missouri Alabama
a 1.0 2.0 NaN NaN
b NaN NaN 7.0 8.0
c 3.0 4.0 9.0 10.0
d NaN NaN 11.0 12.0
e 5.0 6.0 13.0 14.0

DataFrame还有一个便捷的join实例方法,它能更为方便地实现按索引合并。它还可用于合并多个带有相同或相似索引的DataFrame对象,但要求没有重叠的列。

1
left2.join(right2, how='outer')
Ohio Nevada Missouri Alabama
a 1.0 2.0 NaN NaN
b NaN NaN 7.0 8.0
c 3.0 4.0 9.0 10.0
d NaN NaN 11.0 12.0
e 5.0 6.0 13.0 14.0

DataFrame的join方法默认使用的是左连接,保留左边表的行索引。它还支持在调用的DataFrame的列上,连接传递的DataFrame索引:

1
left1.join(right1, on='key')
key value group_val
0 a 0 3.5
1 b 1 7.0
2 a 2 3.5
3 a 3 3.5
4 b 4 7.0
5 c 5 NaN

对于简单的索引合并,你还可以向join传入一组DataFrame

1
2
3
4
another = pd.DataFrame([[7., 8.], [9., 10.], [11., 12.], [16., 17.]],
index=['a', 'c', 'e', 'f'],
columns=['New York', 'Oregon'])
another
New York Oregon
a 7.0 8.0
c 9.0 10.0
e 11.0 12.0
f 16.0 17.0
1
left2.join([right2, another])
Ohio Nevada Missouri Alabama New York Oregon
a 1.0 2.0 NaN NaN 7.0 8.0
c 3.0 4.0 9.0 10.0 9.0 10.0
e 5.0 6.0 13.0 14.0 11.0 12.0

轴向连接

另一种数据合并运算也被称作连接(concatenation)、绑定(binding)或堆叠(stacking)。NumPy的concatenate函数可以用NumPy数组来做:

1
2
arr = np.arange(12).reshape((3, 4))
arr
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
1
np.concatenate([arr, arr], axis=1)
array([[ 0,  1,  2,  3,  0,  1,  2,  3],
       [ 4,  5,  6,  7,  4,  5,  6,  7],
       [ 8,  9, 10, 11,  8,  9, 10, 11]])

对于pandas对象(如Series和DataFrame),带有标签的轴使你能够进一步推广数组的连接运算。
pandas的concat函数提供了一种能够解决这些问题的可靠方式。

1
2
3
4
5
6
7
8
s1 = pd.Series([0, 1], index=['a', 'b'])
s2 = pd.Series([2, 3, 4], index=['c', 'd', 'e'])
s3 = pd.Series([5, 6], index=['f', 'g'])
# 把三个 series 连接在一起
pd.concat([s1, s2, s3])
a    0
b    1
c    2
d    3
e    4
f    5
g    6
dtype: int64

默认情况下,concat是在axis=0上工作的,最终产生一个新的Series。如果传入axis=1,则结果就会变成一个DataFrame(axis=1是列):

1
pd.concat([s1, s2, s3], axis=1)
/usr/local/lib/python3.6/site-packages/ipykernel_launcher.py:1: FutureWarning: Sorting because non-concatenation axis is not aligned. A future version
of pandas will change to not sort by default.

To accept the future behavior, pass 'sort=False'.

To retain the current behavior and silence the warning, pass 'sort=True'.

  """Entry point for launching an IPython kernel.
0 1 2
a 0.0 NaN NaN
b 1.0 NaN NaN
c NaN 2.0 NaN
d NaN 3.0 NaN
e NaN 4.0 NaN
f NaN NaN 5.0
g NaN NaN 6.0

这种情况下,另外的轴上没有重叠,从索引的有序并集(外连接)上就可以看出来。传入join=’inner’即可得到它们的交集:

1
2
s4 = pd.concat([s1, s3])
s4
a    0
b    1
f    5
g    6
dtype: int64
1
pd.concat([s1, s4], axis=1)
/usr/local/lib/python3.6/site-packages/ipykernel_launcher.py:1: FutureWarning: Sorting because non-concatenation axis is not aligned. A future version
of pandas will change to not sort by default.

To accept the future behavior, pass 'sort=False'.

To retain the current behavior and silence the warning, pass 'sort=True'.

  """Entry point for launching an IPython kernel.
0 1
a 0.0 0
b 1.0 1
f NaN 5
g NaN 6
1
pd.concat([s1, s4], axis=1, join='inner')
0 1
a 0 0
b 1 1

通过 join_axes 指定要在其它轴上使用的索引:

1
pd.concat([s1, s4], axis=1, join_axes=[['a', 'c', 'b', 'e']])
0 1
a 0.0 0.0
c NaN NaN
b 1.0 1.0
e NaN NaN

不过有个问题,参与连接的片段在结果中区分不开。假设你想要在连接轴上创建一个层次化索引。使用keys参数即可达到这个目的:

1
2
result = pd.concat([s1, s1, s3], keys=['one','two', 'three'])
result
one    a    0
       b    1
two    a    0
       b    1
three  f    5
       g    6
dtype: int64
1
result.unstack()
a b f g
one 0.0 1.0 NaN NaN
two 0.0 1.0 NaN NaN
three NaN NaN 5.0 6.0

如果沿着 axis=1 对Series进行合并,则keys就会成为DataFrame的列头:

1
pd.concat([s1, s2, s3], axis=1, keys=['one','two', 'three'], sort=True)
one two three
a 0.0 NaN NaN
b 1.0 NaN NaN
c NaN 2.0 NaN
d NaN 3.0 NaN
e NaN 4.0 NaN
f NaN NaN 5.0
g NaN NaN 6.0

同样的逻辑也适用于DataFrame对象:

1
2
df1 = pd.DataFrame(np.arange(6).reshape(3, 2), index=['a', 'b', 'c'], columns=['one', 'two'])
df1
one two
a 0 1
b 2 3
c 4 5
1
2
df2 = pd.DataFrame(5 + np.arange(4).reshape(2, 2), index=['a', 'c'], columns=['three', 'four'])
df2
three four
a 5 6
c 7 8
1
pd.concat([df1, df2], axis=1, keys=['level1', 'level2'], sort = False)
level1 level2
one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0

如果传入的不是列表而是一个字典,则字典的键就会被当做keys选项的值:

1
pd.concat({'level1': df1, 'level2': df2}, axis=1, sort = False)
level1 level2
one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0

我们可以用names参数命名创建的轴级别:

1
pd.concat([df1, df2], axis=1, keys=['level1', 'level2'], names=['upper', 'lower'], sort = False)
upper level1 level2
lower one two three four
a 0 1 5.0 6.0
b 2 3 NaN NaN
c 4 5 7.0 8.0

DataFrame 在连接的时候会使用原来的索引,可以通过 ignore_index=True 来放弃使用

1
2
df1 = pd.DataFrame(np.random.randn(3, 4), columns=['a', 'b', 'c', 'd'])
df1
a b c d
0 -1.044321 -0.049004 0.026555 -0.315565
1 -0.085761 0.873464 -1.368797 0.302554
2 0.277496 -0.570469 -0.606294 1.253497
1
2
df2 = pd.DataFrame(np.random.randn(2, 3), columns=['b', 'd', 'a'])
df2
b d a
0 0.343570 -0.310123 -0.379563
1 -1.083807 -1.480836 -1.255112
1
pd.concat([df1, df2], sort = False)
a b c d
0 -1.044321 -0.049004 0.026555 -0.315565
1 -0.085761 0.873464 -1.368797 0.302554
2 0.277496 -0.570469 -0.606294 1.253497
0 -0.379563 0.343570 NaN -0.310123
1 -1.255112 -1.083807 NaN -1.480836
1
pd.concat([df1, df2], ignore_index=True, sort = False)
a b c d
0 -1.044321 -0.049004 0.026555 -0.315565
1 -0.085761 0.873464 -1.368797 0.302554
2 0.277496 -0.570469 -0.606294 1.253497
3 -0.379563 0.343570 NaN -0.310123
4 -1.255112 -1.083807 NaN -1.480836

concat函数的参数
concat函数的参数

合并重叠数据

还有一种数据组合问题不能用简单的合并(merge)或连接(concatenation)运算来处理。比如说,你可能有索引全部或部分重叠的两个数据集。举个有启发性的例子,我们使用NumPy的where函数,它表示一种等价于面向数组的if-else:

就是用一个数据组合来填充另一个数据组合的空值

1
2
3
4
5
6
a = pd.Series([np.nan, 2.5, np.nan, 3.5, 4.5, np.nan], index=['f', 'e', 'd', 'c', 'b', 'a'])
b = pd.Series(np.arange(len(a), dtype=np.float64), index=['f', 'e', 'd', 'c', 'b', 'a'])
b[-1] = np.nan
a
f    NaN
e    2.5
d    NaN
c    3.5
b    4.5
a    NaN
dtype: float64
1
b
f    0.0
e    1.0
d    2.0
c    3.0
b    4.0
a    NaN
dtype: float64
1
np.where(pd.isnull(a), b, a)
array([0. , 2.5, 2. , 3.5, 4.5, nan])

Series 和 DataFrame 有一个 combine_first 方法,实现的也是一样的功能,还带有pandas的数据对齐:

1
b[:-2].combine_first(a[2:])
a    NaN
b    4.5
c    3.0
d    2.0
e    1.0
f    0.0
dtype: float64
1
2
3
4
df1 = pd.DataFrame({'a': [1., np.nan, 5., np.nan],
'b': [np.nan, 2., np.nan, 6.],
'c': range(2, 18, 4)})
df1
a b c
0 1.0 NaN 2
1 NaN 2.0 6
2 5.0 NaN 10
3 NaN 6.0 14
1
2
3
df2 = pd.DataFrame({'a': [5., 4., np.nan, 3., 7.],
'b': [np.nan, 3., 4., 6., 8.]})
df2
a b
0 5.0 NaN
1 4.0 3.0
2 NaN 4.0
3 3.0 6.0
4 7.0 8.0
1
df1.combine_first(df2)
a b c
0 1.0 NaN 2.0
1 4.0 2.0 6.0
2 5.0 4.0 10.0
3 3.0 6.0 14.0
4 7.0 8.0 NaN

重塑和轴向旋转

有许多用于重新排列表格型数据的基础运算。这些函数也称作重塑(reshape)或轴向旋转(pivot)运算。

重塑层次化索引

层次化索引为DataFrame数据的重排任务提供了一种具有良好一致性的方式。主要功能有二:

  1. stack:将数据的列“旋转”为行。
  2. unstack:将数据的行“旋转”为列。
1
2
3
4
data = pd.DataFrame(np.arange(6).reshape((2, 3)),
index=pd.Index(['Ohio','Colorado'], name='state'),
columns=pd.Index(['one', 'two', 'three'], name='number'))
data
number one two three
state
Ohio 0 1 2
Colorado 3 4 5

对该数据使用stack方法即可将列转换为行,得到一个Series:

1
2
result = data.stack()
result
state     number
Ohio      one       0
          two       1
          three     2
Colorado  one       3
          two       4
          three     5
dtype: int64

对于一个层次化索引的Series,你可以用unstack将其重排为一个DataFrame:

1
result.unstack()
number one two three
state
Ohio 0 1 2
Colorado 3 4 5

默认情况下,unstack操作的是最内层(stack也是如此)。传入分层级别的编号或名称即可对其它级别进行unstack操作:

1
result.unstack(0)
state Ohio Colorado
number
one 0 3
two 1 4
three 2 5
1
result.unstack('state')
state Ohio Colorado
number
one 0 3
two 1 4
three 2 5

如果不是所有的级别值都能在各分组中找到的话,则unstack操作可能会引入缺失数据:

1
2
3
4
5
6
s1 = pd.Series([0, 1, 2, 3], index=['a', 'b', 'c', 'd'])
s2 = pd.Series([4, 5, 6], index=['c', 'd', 'e'])
data2 = pd.concat([s1, s2], keys=['one', 'two'])
data2
one  a    0
     b    1
     c    2
     d    3
two  c    4
     d    5
     e    6
dtype: int64
1
data2.unstack()
a b c d e
one 0.0 1.0 2.0 3.0 NaN
two NaN NaN 4.0 5.0 6.0
1
data2.unstack().stack()
one  a    0.0
     b    1.0
     c    2.0
     d    3.0
two  c    4.0
     d    5.0
     e    6.0
dtype: float64
1
2
# 保留 nan 数据
data2.unstack().stack(dropna=False)
one  a    0.0
     b    1.0
     c    2.0
     d    3.0
     e    NaN
two  a    NaN
     b    NaN
     c    4.0
     d    5.0
     e    6.0
dtype: float64

在对DataFrame进行unstack操作时,作为旋转轴的级别将会成为结果中的最低级别:

1
2
3
df = pd.DataFrame({'left': result, 'right': result + 5},
columns=pd.Index(['left', 'right'], name='side'))
df
side left right
state number
Ohio one 0 5
two 1 6
three 2 7
Colorado one 3 8
two 4 9
three 5 10
1
2
# state 列转换成行的时候,行的级别是最低的(在 side 下面)
df.unstack('state')
side left right
state Ohio Colorado Ohio Colorado
number
one 0 3 5 8
two 1 4 6 9
three 2 5 7 10

当调用stack,我们可以指明轴的名字:

1
df.unstack('state').stack('side')
state Colorado Ohio
number side
one left 3 0
right 8 5
two left 4 1
right 9 6
three left 5 2
right 10 7

将“长格式”旋转为“宽格式”

多个时间序列数据通常是以所谓的“长格式”(long)或“堆叠格式”(stacked)存储在数据库和CSV中的。

1
2
data = pd.read_csv('data/examples/macrodata.csv')
data.head()
year quarter realgdp realcons realinv realgovt realdpi cpi m1 tbilrate unemp pop infl realint
0 1959.0 1.0 2710.349 1707.4 286.898 470.045 1886.9 28.98 139.7 2.82 5.8 177.146 0.00 0.00
1 1959.0 2.0 2778.801 1733.7 310.859 481.301 1919.7 29.15 141.7 3.08 5.1 177.830 2.34 0.74
2 1959.0 3.0 2775.488 1751.8 289.226 491.260 1916.4 29.35 140.5 3.82 5.3 178.657 2.74 1.09
3 1959.0 4.0 2785.204 1753.7 299.356 484.052 1931.3 29.37 140.0 4.33 5.6 179.386 0.27 4.06
4 1960.0 1.0 2847.699 1770.5 331.722 462.199 1955.5 29.54 139.6 3.50 5.2 180.007 2.31 1.19
1
2
3
# 使用几个时间列拼接成一个时间索引
periods = pd.PeriodIndex(year=data.year, quarter=data.quarter, name='date')
periods
PeriodIndex(['1959Q1', '1959Q2', '1959Q3', '1959Q4', '1960Q1', '1960Q2',
             '1960Q3', '1960Q4', '1961Q1', '1961Q2',
             ...
             '2007Q2', '2007Q3', '2007Q4', '2008Q1', '2008Q2', '2008Q3',
             '2008Q4', '2009Q1', '2009Q2', '2009Q3'],
            dtype='period[Q-DEC]', name='date', length=203, freq='Q-DEC')
1
2
3
# 创建索引
columns = pd.Index(['realgdp', 'infl', 'unemp'], name='item')
columns
Index(['realgdp', 'infl', 'unemp'], dtype='object', name='item')
1
2
3
# 选取索引中的那几列
data = data.reindex(columns=columns)
data.head()
item realgdp infl unemp
0 2710.349 0.00 5.8
1 2778.801 2.34 5.1
2 2775.488 2.74 5.3
3 2785.204 0.27 5.6
4 2847.699 2.31 5.2
1
2
3
# 把索引赋值成时间索引
data.index = periods.to_timestamp('D', 'end')
data.head()
item realgdp infl unemp
date
1959-03-31 23:59:59.999999999 2710.349 0.00 5.8
1959-06-30 23:59:59.999999999 2778.801 2.34 5.1
1959-09-30 23:59:59.999999999 2775.488 2.74 5.3
1959-12-31 23:59:59.999999999 2785.204 0.27 5.6
1960-03-31 23:59:59.999999999 2847.699 2.31 5.2

这就是多个时间序列(或者其它带有两个或多个键的可观察数据,这里,我们的键是date和item)的长格式。表中的每行代表一次观察。

1
2
ldata = data.stack().reset_index().rename(columns={0: 'value'})
ldata.head()
date item value
0 1959-03-31 23:59:59.999999999 realgdp 2710.349
1 1959-03-31 23:59:59.999999999 infl 0.000
2 1959-03-31 23:59:59.999999999 unemp 5.800
3 1959-06-30 23:59:59.999999999 realgdp 2778.801
4 1959-06-30 23:59:59.999999999 infl 2.340
1
data.stack().head()
date                           item   
1959-03-31 23:59:59.999999999  realgdp    2710.349
                               infl          0.000
                               unemp         5.800
1959-06-30 23:59:59.999999999  realgdp    2778.801
                               infl          2.340
dtype: float64
1
data.stack().reset_index().head()
date item 0
0 1959-03-31 23:59:59.999999999 realgdp 2710.349
1 1959-03-31 23:59:59.999999999 infl 0.000
2 1959-03-31 23:59:59.999999999 unemp 5.800
3 1959-06-30 23:59:59.999999999 realgdp 2778.801
4 1959-06-30 23:59:59.999999999 infl 2.340

关系型数据库(如MySQL)中的数据经常都是这样存储的,因为固定架构(即列名和数据类型)有一个好处:随着表中数据的添加,item列中的值的种类能够增加。在前面的例子中,date和item通常就是主键(用关系型数据库的说法),不仅提供了关系完整性,而且提供了更为简单的查询支持。有的情况下,使用这样的数据会很麻烦,你可能会更喜欢DataFrame,不同的item值分别形成一列,date列中的时间戳则用作索引。DataFrame的pivot方法完全可以实现这个转换:

1
2
pivoted = ldata.pivot('date', 'item', 'value')
pivoted.head()
item infl realgdp unemp
date
1959-03-31 23:59:59.999999999 0.00 2710.349 5.8
1959-06-30 23:59:59.999999999 2.34 2778.801 5.1
1959-09-30 23:59:59.999999999 2.74 2775.488 5.3
1959-12-31 23:59:59.999999999 0.27 2785.204 5.6
1960-03-31 23:59:59.999999999 2.31 2847.699 5.2

前两个传递的值分别用作行和列索引,最后一个可选值则是用于填充DataFrame的数据列。假设有两个需要同时重塑的数据列:

如果忽略最后一个参数,得到的DataFrame就会带有层次化的列:

1
ldata.pivot('date', 'item').head()
value
item infl realgdp unemp
date
1959-03-31 23:59:59.999999999 0.00 2710.349 5.8
1959-06-30 23:59:59.999999999 2.34 2778.801 5.1
1959-09-30 23:59:59.999999999 2.74 2775.488 5.3
1959-12-31 23:59:59.999999999 0.27 2785.204 5.6
1960-03-31 23:59:59.999999999 2.31 2847.699 5.2
1
2
ldata['value2'] = np.random.randn(len(ldata))
ldata.head()
date item value value2
0 1959-03-31 23:59:59.999999999 realgdp 2710.349 0.363946
1 1959-03-31 23:59:59.999999999 infl 0.000 -0.287075
2 1959-03-31 23:59:59.999999999 unemp 5.800 0.144127
3 1959-06-30 23:59:59.999999999 realgdp 2778.801 -0.491877
4 1959-06-30 23:59:59.999999999 infl 2.340 0.725936
1
ldata.pivot('date', 'item').head()
value value2
item infl realgdp unemp infl realgdp unemp
date
1959-03-31 23:59:59.999999999 0.00 2710.349 5.8 -0.287075 0.363946 0.144127
1959-06-30 23:59:59.999999999 2.34 2778.801 5.1 0.725936 -0.491877 0.026073
1959-09-30 23:59:59.999999999 2.74 2775.488 5.3 1.025040 0.144513 -1.524801
1959-12-31 23:59:59.999999999 0.27 2785.204 5.6 0.530287 -1.027258 -0.158004
1960-03-31 23:59:59.999999999 2.31 2847.699 5.2 -0.476289 0.037717 1.186384
1
ldata.pivot('date', 'item', ['value', 'value2']).head()
value value2
item infl realgdp unemp infl realgdp unemp
date
1959-03-31 23:59:59.999999999 0.00 2710.349 5.8 -0.287075 0.363946 0.144127
1959-06-30 23:59:59.999999999 2.34 2778.801 5.1 0.725936 -0.491877 0.026073
1959-09-30 23:59:59.999999999 2.74 2775.488 5.3 1.025040 0.144513 -1.524801
1959-12-31 23:59:59.999999999 0.27 2785.204 5.6 0.530287 -1.027258 -0.158004
1960-03-31 23:59:59.999999999 2.31 2847.699 5.2 -0.476289 0.037717 1.186384

pivot其实就是用set_index创建层次化索引,再用unstack重塑:

1
2
unstacked = ldata.set_index(['date', 'item']).unstack('item')
unstacked.head()
value value2
item infl realgdp unemp infl realgdp unemp
date
1959-03-31 23:59:59.999999999 0.00 2710.349 5.8 -0.287075 0.363946 0.144127
1959-06-30 23:59:59.999999999 2.34 2778.801 5.1 0.725936 -0.491877 0.026073
1959-09-30 23:59:59.999999999 2.74 2775.488 5.3 1.025040 0.144513 -1.524801
1959-12-31 23:59:59.999999999 0.27 2785.204 5.6 0.530287 -1.027258 -0.158004
1960-03-31 23:59:59.999999999 2.31 2847.699 5.2 -0.476289 0.037717 1.186384

将“宽格式”旋转为“长格式”

旋转DataFrame的逆运算是pandas.melt。它不是将一列转换到多个新的DataFrame,而是合并多个列成为一个,产生一个比输入长的DataFrame。

1
2
3
4
5
df = pd.DataFrame({'key': ['foo', 'bar', 'baz'],
'A': [1, 2, 3],
'B': [4, 5, 6],
'C': [7, 8, 9]})
df
key A B C
0 foo 1 4 7
1 bar 2 5 8
2 baz 3 6 9

key列可能是分组指标,其它的列是数据值。当使用pandas.melt,我们必须指明哪些列是分组指标。下面使用key作为唯一的分组指标:

1
2
melted = pd.melt(df, ['key'])
melted
key variable value
0 foo A 1
1 bar A 2
2 baz A 3
3 foo B 4
4 bar B 5
5 baz B 6
6 foo C 7
7 bar C 8
8 baz C 9

使用pivot,可以重塑回原来的样子:

1
2
reshaped = melted.pivot('key', 'variable', 'value')
reshaped
variable A B C
key
bar 2 5 8
baz 3 6 9
foo 1 4 7

因为pivot的结果从列创建了一个索引,用作行标签,我们可以使用reset_index将数据移回列:

1
reshaped.reset_index()
variable key A B C
0 bar 2 5 8
1 baz 3 6 9
2 foo 1 4 7

还可以指定列的子集,作为值的列:

1
pd.melt(df, id_vars=['key'], value_vars=['A', 'B'])
key variable value
0 foo A 1
1 bar A 2
2 baz A 3
3 foo B 4
4 bar B 5
5 baz B 6

pandas.melt也可以不用分组指标:

1
pd.melt(df, value_vars=['A', 'B', 'C'])
variable value
0 A 1
1 A 2
2 A 3
3 B 4
4 B 5
5 B 6
6 C 7
7 C 8
8 C 9
1
pd.melt(df, value_vars=['key', 'A', 'B'])
variable value
0 key foo
1 key bar
2 key baz
3 A 1
4 A 2
5 A 3
6 B 4
7 B 5
8 B 6

数据聚合与分组运算

关系型数据库和SQL(Structured Query Language,结构化查询语言)能够如此流行的原因之一就是其能够方便地对数据进行连接、过滤、转换和聚合。但是,像SQL这样的查询语言所能执行的分组运算的种类很有限。在本章中你将会看到,由于Python和pandas强大的表达能力,我们可以执行复杂得多的分组运算(利用任何可以接受pandas对象或NumPy数组的函数)。

GroupBy机制

Hadley Wickham(许多热门R语言包的作者)创造了一个用于表示分组运算的术语”split-apply-combine”(拆分-应用-合并)。第一个阶段,pandas对象(无论是Series、DataFrame还是其他的)中的数据会根据你所提供的一个或多个键被拆分(split)为多组。拆分操作是在对象的特定轴上执行的。例如,DataFrame可以在其行(axis=0)或列(axis=1)上进行分组。然后,将一个函数应用(apply)到各个分组并产生一个新值。最后,所有这些函数的执行结果会被合并(combine)到最终的结果对象中。结果对象的形式一般取决于数据上所执行的操作。图10-1大致说明了一个简单的分组聚合过程。
图10-1

1
2
3
4
5
df = pd.DataFrame({'key1' : ['a', 'a', 'b', 'b', 'a'],
'key2' : ['one', 'two', 'one', 'two', 'one'],
'data1' : np.random.randn(5),
'data2' : np.random.randn(5)})
df
key1 key2 data1 data2
0 a one 1.192962 -1.675544
1 a two 1.295617 -0.621154
2 b one 2.506938 -0.954289
3 b two -0.503276 -0.698834
4 a one -0.204452 0.767441

假设你想要按key1进行分组,并计算data1列的平均值。实现该功能的方式有很多,而我们这里要用的是:访问data1,并根据key1调用groupby:

1
2
grouped = df['data1'].groupby(df['key1'])
grouped
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x00000000080736D8>

变量 grouped 是一个 GroupBy 对象。它实际上还没有进行任何计算,只是含有一些有关分组键 df['key1'] 的中间数据而已。换句话说,该对象已经有了接下来对各分组执行运算所需的一切信息。例如,我们可以调用 GroupBymean 方法来计算分组平均值:

1
grouped.mean()
key1
a    0.761376
b    1.001831
Name: data1, dtype: float64

数据(Series)根据分组键进行了聚合,产生了一个新的Series,其索引为key1列中的唯一值。之所以结果中索引的名称为key1,是因为原始DataFrame的列df[‘key1’]就叫这个名字。

使用多个分组字段

1
2
means = df['data1'].groupby([df['key1'], df['key2']]).mean()
means
key1  key2
a     one     0.494255
      two     1.295617
b     one     2.506938
      two    -0.503276
Name: data1, dtype: float64

这里,我通过两个键对数据进行了分组,得到的Series具有一个层次化索引(由唯一的键对组成)

1
means.unstack()
key2 one two
key1
a 0.494255 1.295617
b 2.506938 -0.503276

分组键可以是任何长度适当 (等于要分组数据的长度)的数组:

1
2
3
4
5
states = np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])
years = np.array([2005, 2005, 2006, 2005, 2006])
df['data1'].groupby([states, years]).mean()
California  2005    1.295617
            2006    2.506938
Ohio        2005    0.344843
            2006   -0.204452
Name: data1, dtype: float64

通常,分组信息就位于相同的要处理DataFrame中。这里,你还可以将列名(可以是字符串、数字或其他Python对象)用作分组键:

1
df.groupby('key1').mean()
data1 data2
key1
a 0.761376 -0.509753
b 1.001831 -0.826561
1
df.groupby(['key1', 'key2']).mean()
data1 data2
key1 key2
a one 0.494255 -0.454052
two 1.295617 -0.621154
b one 2.506938 -0.954289
two -0.503276 -0.698834

你可能已经注意到了,第一个例子在执行df.groupby(‘key1’).mean()时,结果中没有key2列。这是因为df[‘key2’]不是数值数据(俗称“麻烦列”),所以被从结果中排除了。默认情况下,所有数值列都会被聚合,虽然有时可能会被过滤为一个子集,稍后就会碰到。

无论你准备拿 groupby 做什么,都有可能会用到 GroupBy 的 size 方法,它可以返回一个含有分组大小的 Series:

1
df.groupby(['key1', 'key2']).size()
key1  key2
a     one     2
      two     1
b     one     1
      two     1
dtype: int64

注意,任何分组关键词中的缺失值,都会被从结果中除去。

对分组进行迭代

GroupBy对象支持迭代,可以产生一组二元元组(由分组名和数据块组成)。

1
df
key1 key2 data1 data2
0 a one 1.192962 -1.675544
1 a two 1.295617 -0.621154
2 b one 2.506938 -0.954289
3 b two -0.503276 -0.698834
4 a one -0.204452 0.767441
1
2
3
for name, group in df.groupby('key1'):
print(name)
print(group)
a
  key1 key2     data1     data2
0    a  one  1.192962 -1.675544
1    a  two  1.295617 -0.621154
4    a  one -0.204452  0.767441
b
  key1 key2     data1     data2
2    b  one  2.506938 -0.954289
3    b  two -0.503276 -0.698834

对于多重键的情况,元组的第一个元素将会是由键值组成的元组:

1
2
3
for (k1, k2), group in df.groupby(['key1', 'key2']):
print((k1, k2))
print(group)
('a', 'one')
  key1 key2     data1     data2
0    a  one  1.192962 -1.675544
4    a  one -0.204452  0.767441
('a', 'two')
  key1 key2     data1     data2
1    a  two  1.295617 -0.621154
('b', 'one')
  key1 key2     data1     data2
2    b  one  2.506938 -0.954289
('b', 'two')
  key1 key2     data1     data2
3    b  two -0.503276 -0.698834

你可以对这些数据片段做任何操作。有一个你可能会觉得有用的运算:将这些数据片段做成一个字典:

1
2
pieces = dict(list(df.groupby('key1')))
pieces['b']
key1 key2 data1 data2
2 b one 2.506938 -0.954289
3 b two -0.503276 -0.698834

groupby默认是在 axis=0 上进行分组的,通过设置也可以在其他任何轴上进行分组。拿上面例子中的df来说,我们可以根据dtype对列进行分组:

1
df.dtypes
key1      object
key2      object
data1    float64
data2    float64
dtype: object
1
2
3
4
5
# 按照数据类型分组
grouped = df.groupby(df.dtypes, axis=1)
for dtype, group in grouped:
print(dtype)
print(group)
float64
      data1     data2
0  1.192962 -1.675544
1  1.295617 -0.621154
2  2.506938 -0.954289
3 -0.503276 -0.698834
4 -0.204452  0.767441
object
  key1 key2
0    a  one
1    a  two
2    b  one
3    b  two
4    a  one

选取一列或列的子集

对于由DataFrame产生的GroupBy对象,如果用一个(单个字符串)或一组(字符串数组)列名对其进行索引,就能实现选取部分列进行聚合的目的。

1
2
df.groupby('key1')['data1']
df.groupby('key1')[['data2']]

是以下代码的语法糖:

1
2
df['data1'].groupby(df['key1'])
df[['data2']].groupby(df['key1'])

尤其对于大数据集,很可能只需要对部分列进行聚合。
例如,在前面那个数据集中,如果只需计算data2列的平均值并以DataFrame形式得到结果,可以这样写:

1
2
# 双 [[]] 要生成 dataframe
df.groupby(['key1', 'key2'])[['data2']].mean()
data2
key1 key2
a one -0.454052
two -0.621154
b one -0.954289
two -0.698834
1
2
# 单 [] 要生成 series
df.groupby(['key1', 'key2'])['data2'].mean()
key1  key2
a     one    -0.454052
      two    -0.621154
b     one    -0.954289
      two    -0.698834
Name: data2, dtype: float64

通过字典或Series进行分组

1
2
3
4
5
people = pd.DataFrame(np.random.randn(5, 5),
columns=['a', 'b', 'c', 'd', 'e'],
index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])
people.iloc[2:3, [1, 2]] = np.nan
people
a b c d e
Joe 1.259713 -0.377088 0.520075 -0.881195 0.158433
Steve -1.226603 0.648477 -0.317307 0.012993 0.584107
Wes -1.811950 NaN NaN -0.576653 0.362209
Jim 0.951494 0.253198 -0.386507 1.172929 -1.755465
Travis 1.107800 0.297369 -0.279916 1.512150 0.755150

假设已知列的分组关系,并希望根据分组计算列的和:

1
mapping = {'a': 'red', 'b': 'red', 'c': 'blue', 'd': 'blue', 'e': 'red', 'f' : 'orange'}

将这个字典传给groupby,来构造数组,但我们可以直接传递字典(我包含了键“f”来强调,存在未使用的分组键是可以的):

1
2
by_column = people.groupby(mapping, axis=1)
by_column.sum()
blue red
Joe -0.361120 1.041059
Steve -0.304314 0.005981
Wes -0.576653 -1.449741
Jim 0.786422 -0.550774
Travis 1.232234 2.160319

Series也有同样的功能,它可以被看做一个固定大小的映射:

1
2
map_series = pd.Series(mapping)
map_series
a       red
b       red
c      blue
d      blue
e       red
f    orange
dtype: object
1
people.groupby(map_series, axis=1).count()
blue red
Joe 2 3
Steve 2 3
Wes 1 2
Jim 2 3
Travis 2 3

通过函数进行分组

比起使用字典或Series,使用Python函数是一种更原生的方法定义分组映射。任何被当做分组键的函数都会在各个索引值上被调用一次,其返回值就会被用作分组名称。

1
people
a b c d e
Joe 1.259713 -0.377088 0.520075 -0.881195 0.158433
Steve -1.226603 0.648477 -0.317307 0.012993 0.584107
Wes -1.811950 NaN NaN -0.576653 0.362209
Jim 0.951494 0.253198 -0.386507 1.172929 -1.755465
Travis 1.107800 0.297369 -0.279916 1.512150 0.755150

其索引值为人的名字。你可以计算一个字符串长度的数组,更简单的方法是传入 len 函数:

1
people.groupby(len).sum()
a b c d e
3 0.399257 -0.123890 0.133568 -0.284919 -1.234823
5 -1.226603 0.648477 -0.317307 0.012993 0.584107
6 1.107800 0.297369 -0.279916 1.512150 0.755150

将函数跟数组、列表、字典、Series混合使用也不是问题,因为任何东西在内部都会被转换为数组:

1
2
key_list = ['one', 'one', 'one', 'two', 'two']
people.groupby([len, key_list]).min()
a b c d e
3 one -1.811950 -0.377088 0.520075 -0.881195 0.158433
two 0.951494 0.253198 -0.386507 1.172929 -1.755465
5 one -1.226603 0.648477 -0.317307 0.012993 0.584107
6 two 1.107800 0.297369 -0.279916 1.512150 0.755150

根据索引级别分组

层次化索引数据集最方便的地方就在于它能够根据轴索引的一个级别进行聚合:

1
2
3
4
5
columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],
[1, 3, 5, 1, 3]],
names=['cty', 'tenor'])
hier_df = pd.DataFrame(np.random.randn(4, 5), columns=columns)
hier_df
cty US JP
tenor 1 3 5 1 3
0 -0.676690 -0.294463 0.275278 -0.315009 -0.454633
1 -1.509024 0.474617 -0.969700 -0.043906 -1.237097
2 0.379676 -0.577742 1.084988 0.499930 0.373462
3 1.097124 -0.437426 0.725242 -1.646882 0.571528

要根据级别分组,使用 level 关键字传递级别序号或名字:

1
hier_df.groupby(level='cty', axis=1).count()
cty JP US
0 2 3
1 2 3
2 2 3
3 2 3

数据聚合

聚合指的是任何能够从数组产生标量值的数据转换过程。之前的例子已经用过一些,比如mean、count、min以及sum等。
常见的聚合运算
常用的聚合运算方法

1
df
key1 key2 data1 data2
0 a one 1.192962 -1.675544
1 a two 1.295617 -0.621154
2 b one 2.506938 -0.954289
3 b two -0.503276 -0.698834
4 a one -0.204452 0.767441
1
grouped = df.groupby('key1')

如果要使用你自己的聚合函数,只需将其传入 aggregate 或 agg 方法即可:

1
2
3
def peak_to_peak(arr):
return arr.max() - arr.min()
grouped.agg(peak_to_peak)
data1 data2
key1
a 1.500069 2.442985
b 3.010214 0.255455

获取分组后的描述信息

1
grouped.describe()
data1 data2
count mean std min 25% 50% 75% max count mean std min 25% 50% 75% max
key1
a 3.0 0.761376 0.838004 -0.204452 0.494255 1.192962 1.244290 1.295617 3.0 -0.509753 1.225297 -1.675544 -1.148349 -0.621154 0.073143 0.767441
b 2.0 1.001831 2.128543 -0.503276 0.249277 1.001831 1.754384 2.506938 2.0 -0.826561 0.180634 -0.954289 -0.890425 -0.826561 -0.762697 -0.698834

自定义聚合函数要比图中那些经过优化的函数慢得多。这是因为在构造中间分组数据块时存在非常大的开销(函数调用、数据重排等)。

面向列的多函数应用

你可能希望对不同的列使用不同的聚合函数,或一次应用多个函数。其实这也好办,我将通过一些示例来进行讲解。

1
2
3
tips = pd.read_csv('data/examples/tips.csv')
tips['tip_pct'] = tips['tip'] / tips['total_bill']
tips.head()
total_bill tip smoker day time size tip_pct
0 16.99 1.01 No Sun Dinner 2 0.059447
1 10.34 1.66 No Sun Dinner 3 0.160542
2 21.01 3.50 No Sun Dinner 3 0.166587
3 23.68 3.31 No Sun Dinner 2 0.139780
4 24.59 3.61 No Sun Dinner 4 0.146808

首先,我根据天和smoker对tips进行分组:

1
grouped = tips.groupby(['day', 'smoker'])

获取分组后的一列

1
grouped_pct = grouped['tip_pct']

按照指定的方式对这一列分组进行结算

1
grouped_pct.agg('mean')
day   smoker
Fri   No        0.151650
      Yes       0.174783
Sat   No        0.158048
      Yes       0.147906
Sun   No        0.160113
      Yes       0.187250
Thur  No        0.160298
      Yes       0.163863
Name: tip_pct, dtype: float64

如果传入一组函数或函数名,得到的DataFrame的列就会以相应的函数命名:

1
2
# peak_to_peak 是上面自定义的一个方法
grouped_pct.agg(['mean', 'std', peak_to_peak])
mean std peak_to_peak
day smoker
Fri No 0.151650 0.028123 0.067349
Yes 0.174783 0.051293 0.159925
Sat No 0.158048 0.039767 0.235193
Yes 0.147906 0.061375 0.290095
Sun No 0.160113 0.042347 0.193226
Yes 0.187250 0.154134 0.644685
Thur No 0.160298 0.038774 0.193350
Yes 0.163863 0.039389 0.151240

你并非一定要接受GroupBy自动给出的那些列名,特别是lambda函数,它们的名称是’’,这样的辨识度就很低了(通过函数的name属性看看就知道了)。因此,如果传入的是一个由(name,function)元组组成的列表,则各元组的第一个元素就会被用作DataFrame的列名(可以将这种二元元组列表看做一个有序映射):

设置方法的字段别名

1
grouped_pct.agg([('foo', 'mean'), ('bar', np.std)])
foo bar
day smoker
Fri No 0.151650 0.028123
Yes 0.174783 0.051293
Sat No 0.158048 0.039767
Yes 0.147906 0.061375
Sun No 0.160113 0.042347
Yes 0.187250 0.154134
Thur No 0.160298 0.038774
Yes 0.163863 0.039389

对于DataFrame,你还有更多选择,你可以定义一组应用于全部列的一组函数,或不同的列应用不同的函数。假设我们想要对tip_pct和total_bill列计算三个统计信息:

1
2
3
4
5
functions = ['count', 'mean', 'max']
result = grouped['tip_pct', 'total_bill'].agg(functions)
result
tip_pct total_bill
count mean max count mean max
day smoker
Fri No 4 0.151650 0.187735 4 18.420000 22.75
Yes 15 0.174783 0.263480 15 16.813333 40.17
Sat No 45 0.158048 0.291990 45 19.661778 48.33
Yes 42 0.147906 0.325733 42 21.276667 50.81
Sun No 57 0.160113 0.252672 57 20.506667 48.17
Yes 19 0.187250 0.710345 19 24.120000 45.35
Thur No 45 0.160298 0.266312 45 17.113111 41.19
Yes 17 0.163863 0.241255 17 19.190588 43.11
1
result['tip_pct']
count mean max
day smoker
Fri No 4 0.151650 0.187735
Yes 15 0.174783 0.263480
Sat No 45 0.158048 0.291990
Yes 42 0.147906 0.325733
Sun No 57 0.160113 0.252672
Yes 19 0.187250 0.710345
Thur No 45 0.160298 0.266312
Yes 17 0.163863 0.241255

跟前面一样,这里也可以传入带有自定义名称的一组元组:

1
2
3
ftuples = [('Durchschnitt', 'mean'),('Abweichung', np.var)]
grouped['tip_pct', 'total_bill'].agg(ftuples)
tip_pct total_bill
Durchschnitt Abweichung Durchschnitt Abweichung
day smoker
Fri No 0.151650 0.000791 18.420000 25.596333
Yes 0.174783 0.002631 16.813333 82.562438
Sat No 0.158048 0.001581 19.661778 79.908965
Yes 0.147906 0.003767 21.276667 101.387535
Sun No 0.160113 0.001793 20.506667 66.099980
Yes 0.187250 0.023757 24.120000 109.046044
Thur No 0.160298 0.001503 17.113111 59.625081
Yes 0.163863 0.001551 19.190588 69.808518

现在,假设你想要对一个列或不同的列应用不同的函数。具体的办法是向agg传入一个从列名映射到函数的字典:

1
2
# 分组后,对 tip 列进行 np.max 运算; size 列进行 sum 运算
grouped.agg({'tip' : np.max, 'size' : 'sum'})
tip size
day smoker
Fri No 3.50 9
Yes 4.73 31
Sat No 9.00 115
Yes 10.00 104
Sun No 6.00 167
Yes 6.50 49
Thur No 6.70 112
Yes 5.00 40
1
2
# 分组后,对 tip_pct 列分别进行 多种 运算; size 列进行 sum 运算
grouped.agg({'tip_pct' : ['min', 'max', 'mean', 'std'], 'size' : 'sum'})
tip_pct size
min max mean std sum
day smoker
Fri No 0.120385 0.187735 0.151650 0.028123 9
Yes 0.103555 0.263480 0.174783 0.051293 31
Sat No 0.056797 0.291990 0.158048 0.039767 115
Yes 0.035638 0.325733 0.147906 0.061375 104
Sun No 0.059447 0.252672 0.160113 0.042347 167
Yes 0.065660 0.710345 0.187250 0.154134 49
Thur No 0.072961 0.266312 0.160298 0.038774 112
Yes 0.090014 0.241255 0.163863 0.039389 40

只有将多个函数应用到至少一列时,DataFrame才会拥有层次化的列。

以“没有行索引”的形式返回聚合数据

默认返回的数据是带分组索引的

1
tips.groupby(['day', 'smoker']).mean()
total_bill tip size tip_pct
day smoker
Fri No 18.420000 2.812500 2.250000 0.151650
Yes 16.813333 2.714000 2.066667 0.174783
Sat No 19.661778 3.102889 2.555556 0.158048
Yes 21.276667 2.875476 2.476190 0.147906
Sun No 20.506667 3.167895 2.929825 0.160113
Yes 24.120000 3.516842 2.578947 0.187250
Thur No 17.113111 2.673778 2.488889 0.160298
Yes 19.190588 3.030000 2.352941 0.163863

可以向 groupby 传入 as_index=False 以禁用该功能

1
tips.groupby(['day', 'smoker'], as_index=False).mean()
day smoker total_bill tip size tip_pct
0 Fri No 18.420000 2.812500 2.250000 0.151650
1 Fri Yes 16.813333 2.714000 2.066667 0.174783
2 Sat No 19.661778 3.102889 2.555556 0.158048
3 Sat Yes 21.276667 2.875476 2.476190 0.147906
4 Sun No 20.506667 3.167895 2.929825 0.160113
5 Sun Yes 24.120000 3.516842 2.578947 0.187250
6 Thur No 17.113111 2.673778 2.488889 0.160298
7 Thur Yes 19.190588 3.030000 2.352941 0.163863

apply:一般性的“拆分-应用-合并”

apply会将待处理的对象拆分成多个片段,然后对各片段调用传入的函数,最后尝试将各片段组合到一起。

1
tips.head()
total_bill tip smoker day time size tip_pct
0 16.99 1.01 No Sun Dinner 2 0.059447
1 10.34 1.66 No Sun Dinner 3 0.160542
2 21.01 3.50 No Sun Dinner 3 0.166587
3 23.68 3.31 No Sun Dinner 2 0.139780
4 24.59 3.61 No Sun Dinner 4 0.146808

假设你想要根据分组选出最高的5个tip_pct值。首先,编写一个选取指定列具有最大值的行的函数:

1
2
3
def top(df, n=5, column='tip_pct'):
return df.sort_values(by=column)[-n:]
top(tips, n = 6)
total_bill tip smoker day time size tip_pct
109 14.31 4.00 Yes Sat Dinner 2 0.279525
183 23.17 6.50 Yes Sun Dinner 4 0.280535
232 11.61 3.39 No Sat Dinner 2 0.291990
67 3.07 1.00 Yes Sat Dinner 1 0.325733
178 9.60 4.00 Yes Sun Dinner 2 0.416667
172 7.25 5.15 Yes Sun Dinner 2 0.710345

如果对smoker分组并用该函数调用apply,就会得到:

1
tips.groupby('smoker').apply(top)
total_bill tip smoker day time size tip_pct
smoker
No 88 24.71 5.85 No Thur Lunch 2 0.236746
185 20.69 5.00 No Sun Dinner 5 0.241663
51 10.29 2.60 No Sun Dinner 2 0.252672
149 7.51 2.00 No Thur Lunch 2 0.266312
232 11.61 3.39 No Sat Dinner 2 0.291990
Yes 109 14.31 4.00 Yes Sat Dinner 2 0.279525
183 23.17 6.50 Yes Sun Dinner 4 0.280535
67 3.07 1.00 Yes Sat Dinner 1 0.325733
178 9.60 4.00 Yes Sun Dinner 2 0.416667
172 7.25 5.15 Yes Sun Dinner 2 0.710345

top函数在DataFrame的各个片段上调用,然后结果由pandas.concat组装到一起,并以分组名称进行了标记。于是,最终结果就有了一个层次化索引,其内层索引值来自原DataFrame。

如果传给apply的函数能够接受其他参数或关键字,则可以将这些内容放在函数名后面一并传入:

1
tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill')
total_bill tip smoker day time size tip_pct
smoker day
No Fri 94 22.75 3.25 No Fri Dinner 2 0.142857
Sat 212 48.33 9.00 No Sat Dinner 4 0.186220
Sun 156 48.17 5.00 No Sun Dinner 6 0.103799
Thur 142 41.19 5.00 No Thur Lunch 5 0.121389
Yes Fri 95 40.17 4.73 Yes Fri Dinner 4 0.117750
Sat 170 50.81 10.00 Yes Sat Dinner 3 0.196812
Sun 182 45.35 3.50 Yes Sun Dinner 3 0.077178
Thur 197 43.11 5.00 Yes Thur Lunch 4 0.115982

在GroupBy中,当你调用诸如describe之类的方法时,实际上只是应用了下面两条代码的快捷方式而已:

1
2
result = tips.groupby('smoker')['tip_pct'].describe()
result
count mean std min 25% 50% 75% max
smoker
No 151.0 0.159328 0.039910 0.056797 0.136906 0.155625 0.185014 0.291990
Yes 93.0 0.163196 0.085119 0.035638 0.106771 0.153846 0.195059 0.710345
1
result.unstack('smoker')
       smoker
count  No        151.000000
       Yes        93.000000
mean   No          0.159328
       Yes         0.163196
std    No          0.039910
       Yes         0.085119
min    No          0.056797
       Yes         0.035638
25%    No          0.136906
       Yes         0.106771
50%    No          0.155625
       Yes         0.153846
75%    No          0.185014
       Yes         0.195059
max    No          0.291990
       Yes         0.710345
dtype: float64
1
result.unstack('smoker').swaplevel(0, 1)
smoker       
No      count    151.000000
Yes     count     93.000000
No      mean       0.159328
Yes     mean       0.163196
No      std        0.039910
Yes     std        0.085119
No      min        0.056797
Yes     min        0.035638
No      25%        0.136906
Yes     25%        0.106771
No      50%        0.155625
Yes     50%        0.153846
No      75%        0.185014
Yes     75%        0.195059
No      max        0.291990
Yes     max        0.710345
dtype: float64

applay 的实现方式

1
2
f = lambda x: x.describe()
tips.groupby('smoker')['tip_pct'].apply(f)
smoker       
No      count    151.000000
        mean       0.159328
        std        0.039910
        min        0.056797
        25%        0.136906
        50%        0.155625
        75%        0.185014
        max        0.291990
Yes     count     93.000000
        mean       0.163196
        std        0.085119
        min        0.035638
        25%        0.106771
        50%        0.153846
        75%        0.195059
        max        0.710345
Name: tip_pct, dtype: float64

禁止分组键

分组键会跟原始对象的索引共同构成结果对象中的层次化索引。将 group_keys=False 传入 groupby 即可禁止该效果:

1
tips.groupby('smoker').apply(top)
total_bill tip smoker day time size tip_pct
smoker
No 88 24.71 5.85 No Thur Lunch 2 0.236746
185 20.69 5.00 No Sun Dinner 5 0.241663
51 10.29 2.60 No Sun Dinner 2 0.252672
149 7.51 2.00 No Thur Lunch 2 0.266312
232 11.61 3.39 No Sat Dinner 2 0.291990
Yes 109 14.31 4.00 Yes Sat Dinner 2 0.279525
183 23.17 6.50 Yes Sun Dinner 4 0.280535
67 3.07 1.00 Yes Sat Dinner 1 0.325733
178 9.60 4.00 Yes Sun Dinner 2 0.416667
172 7.25 5.15 Yes Sun Dinner 2 0.710345
1
tips.groupby('smoker', group_keys=False).apply(top)
total_bill tip smoker day time size tip_pct
88 24.71 5.85 No Thur Lunch 2 0.236746
185 20.69 5.00 No Sun Dinner 5 0.241663
51 10.29 2.60 No Sun Dinner 2 0.252672
149 7.51 2.00 No Thur Lunch 2 0.266312
232 11.61 3.39 No Sat Dinner 2 0.291990
109 14.31 4.00 Yes Sat Dinner 2 0.279525
183 23.17 6.50 Yes Sun Dinner 4 0.280535
67 3.07 1.00 Yes Sat Dinner 1 0.325733
178 9.60 4.00 Yes Sun Dinner 2 0.416667
172 7.25 5.15 Yes Sun Dinner 2 0.710345

分位数和桶分析

pandas有一些能根据指定面元或样本分位数将数据拆分成多块的工具(比如cut和qcut)。将这些函数跟groupby结合起来,就能非常轻松地实现对数据集的桶(bucket)或分位数(quantile)分析了。以下面这个简单的随机数据集为例,我们利用cut将其装入长度相等的桶中:

1
2
3
4
frame = pd.DataFrame({'data1': np.random.randn(1000),
'data2': np.random.randn(1000)})
quartiles = pd.cut(frame.data1, 4)
quartiles.head()
0    (-1.698, 0.0798]
1     (0.0798, 1.857]
2    (-1.698, 0.0798]
3     (0.0798, 1.857]
4    (-1.698, 0.0798]
Name: data1, dtype: category
Categories (4, interval[float64]): [(-3.482, -1.698] < (-1.698, 0.0798] < (0.0798, 1.857] < (1.857, 3.635]]

由cut返回的Categorical对象可直接传递到groupby。因此,我们可以像下面这样对data2列做一些统计计算:

1
2
3
4
5
def get_stats(group):
return {'min': group.min(), 'max': group.max(), 'count': group.count(), 'mean': group.mean()}
grouped = frame.data2.groupby(quartiles)
grouped.apply(get_stats).unstack()
count max mean min
data1
(-3.482, -1.698] 38.0 2.161012 0.286213 -1.820655
(-1.698, 0.0798] 472.0 3.762126 -0.044402 -3.260810
(0.0798, 1.857] 452.0 3.026034 -0.040110 -3.122991
(1.857, 3.635] 38.0 1.482856 -0.191989 -1.969943

这些都是长度相等的桶。要根据样本分位数得到大小相等的桶,使用qcut即可。传入 labels=False 即可只获取分位数的编号:

1
2
grouping = pd.qcut(frame.data1, 10, labels=False)
grouping.head()
0    1
1    5
2    3
3    5
4    3
Name: data1, dtype: int64
1
2
temp = pd.qcut(frame.data1, 10)
temp.head()
0    (-1.261, -0.827]
1     (0.0684, 0.326]
2    (-0.476, -0.193]
3     (0.0684, 0.326]
4    (-0.476, -0.193]
Name: data1, dtype: category
Categories (10, interval[float64]): [(-3.476, -1.261] < (-1.261, -0.827] < (-0.827, -0.476] < (-0.476, -0.193] ... (0.326, 0.596] < (0.596, 0.925] < (0.925, 1.368] < (1.368, 3.635]]
1
2
grouped = frame.data2.groupby(grouping)
grouped.apply(get_stats).unstack()
count max mean min
data1
0 100.0 2.621324 0.070152 -2.179777
1 100.0 2.462273 -0.049077 -2.289337
2 100.0 3.762126 -0.077355 -3.260810
3 100.0 2.707121 0.016885 -2.551321
4 100.0 1.805644 -0.043425 -2.499053
5 100.0 2.079105 -0.021532 -2.564312
6 100.0 3.026034 -0.123689 -3.122991
7 100.0 2.220393 -0.105029 -2.630459
8 100.0 2.454573 -0.046064 -2.195948
9 100.0 2.476637 0.024066 -2.050193

示例:用特定于分组的值填充缺失值

对于缺失数据的清理工作,有时你会用dropna将其替换掉,而有时则可能会希望用一个固定值或由数据集本身所衍生出来的值去填充NA值。这时就得使用fillna这个工具了。在下面这个例子中,我用平均值去填充NA值:

1
2
3
s = pd.Series(np.random.randn(6))
s[::2] = np.nan
s
0         NaN
1   -1.698228
2         NaN
3    0.250362
4         NaN
5   -0.981594
dtype: float64
1
s.fillna(s.mean())
0   -0.809820
1   -1.698228
2   -0.809820
3    0.250362
4   -0.809820
5   -0.981594
dtype: float64

假设你需要对不同的分组填充不同的值。一种方法是将数据分组,并使用apply和一个能够对各数据块调用fillna的函数即可。

1
2
3
4
5
states = ['Ohio', 'New York', 'Vermont', 'Florida', 'Oregon', 'Nevada', 'California', 'Idaho']
group_key = ['East'] * 4 + ['West'] * 4
group_key
['East', 'East', 'East', 'East', 'West', 'West', 'West', 'West']
1
2
data = pd.Series(np.random.randn(8), index=states)
data
Ohio         -0.343101
New York      1.024389
Vermont       1.311144
Florida      -0.423204
Oregon        0.114283
Nevada       -1.089131
California   -1.727345
Idaho        -0.830830
dtype: float64
1
2
data[['Vermont', 'Nevada', 'Idaho']] = np.nan
data
Ohio         -0.343101
New York      1.024389
Vermont            NaN
Florida      -0.423204
Oregon        0.114283
Nevada             NaN
California   -1.727345
Idaho              NaN
dtype: float64
1
data.groupby(group_key).mean()
East    0.086028
West   -0.806531
dtype: float64

我们可以用分组平均值去填充NA值:

1
2
3
fill_mean = lambda g: g.fillna(g.mean())
data.groupby(group_key).apply(fill_mean)
Ohio         -0.343101
New York      1.024389
Vermont       0.086028
Florida      -0.423204
Oregon        0.114283
Nevada       -0.806531
California   -1.727345
Idaho        -0.806531
dtype: float64

也可以在代码中预定义各组的填充值。由于分组具有一个name属性,所以我们可以拿来用一下:

1
2
3
4
5
fill_values = {'East': 0.5, 'West': -1}
fill_func = lambda g: g.fillna(fill_values[g.name])
data.groupby(group_key).apply(fill_func)
Ohio         -0.343101
New York      1.024389
Vermont       0.500000
Florida      -0.423204
Oregon        0.114283
Nevada       -1.000000
California   -1.727345
Idaho        -1.000000
dtype: float64

示例:分组加权平均数和相关系数

根据groupby的“拆分-应用-合并”范式,可以进行DataFrame的列与列之间或两个Series之间的运算(比如分组加权平均)。以下面这个数据集为例,它含有分组键、值以及一些权重值:

1
2
3
4
df = pd.DataFrame({'category': ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'b'],
'data': np.random.randn(8),
'weights': np.random.rand(8)})
df
category data weights
0 a -0.980463 0.247153
1 a 0.148452 0.912352
2 a -1.067199 0.719493
3 a 1.837065 0.883523
4 b -1.030439 0.907246
5 b 0.120672 0.538387
6 b -0.306529 0.423223
7 b 0.709020 0.552725

利用 category 计算分组加权平均数:

1
2
3
4
5
grouped = df.groupby('category')
get_wavg = lambda g: np.average(g['data'], weights=g['weights'])
grouped.apply(get_wavg)
category
a    0.270898
b   -0.250964
dtype: float64

另一个例子,考虑一个来自Yahoo!Finance的数据集,其中含有几只股票和标准普尔500指数(符号SPX)的收盘价:

1
2
close_px = pd.read_csv('data/examples/stock_px_2.csv', parse_dates=True, index_col=0)
close_px.info()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 2214 entries, 2003-01-02 to 2011-10-14
Data columns (total 4 columns):
AAPL    2214 non-null float64
MSFT    2214 non-null float64
XOM     2214 non-null float64
SPX     2214 non-null float64
dtypes: float64(4)
memory usage: 86.5 KB
1
close_px.head()
AAPL MSFT XOM SPX
2003-01-02 7.40 21.11 29.22 909.03
2003-01-03 7.45 21.14 29.24 908.59
2003-01-06 7.45 21.52 29.96 929.01
2003-01-07 7.43 21.93 28.95 922.93
2003-01-08 7.28 21.31 28.83 909.93

来做一个比较有趣的任务:计算一个由日收益率(通过百分数变化计算)与SPX之间的年度相关系数组成的DataFrame。下面是一个实现办法,我们先创建一个函数,用它计算每列和SPX列的成对相关系数:

1
2
3
4
spx_corr = lambda x: x.corrwith(x['SPX'])
# 我们使用pct_change计算close_px的百分比变化
rets = close_px.pct_change().dropna()
rets.head()
AAPL MSFT XOM SPX
2003-01-03 0.006757 0.001421 0.000684 -0.000484
2003-01-06 0.000000 0.017975 0.024624 0.022474
2003-01-07 -0.002685 0.019052 -0.033712 -0.006545
2003-01-08 -0.020188 -0.028272 -0.004145 -0.014086
2003-01-09 0.008242 0.029094 0.021159 0.019386

最后,我们用年对百分比变化进行分组,可以用一个一行的函数,从每行的标签返回每个datetime标签的year属性:

1
2
3
4
5
get_year = lambda x: x.year
by_year = rets.groupby(get_year)
by_year.apply(spx_corr)
AAPL MSFT XOM SPX
2003 0.541124 0.745174 0.661265 1.0
2004 0.374283 0.588531 0.557742 1.0
2005 0.467540 0.562374 0.631010 1.0
2006 0.428267 0.406126 0.518514 1.0
2007 0.508118 0.658770 0.786264 1.0
2008 0.681434 0.804626 0.828303 1.0
2009 0.707103 0.654902 0.797921 1.0
2010 0.710105 0.730118 0.839057 1.0
2011 0.691931 0.800996 0.859975 1.0

还可以计算列与列之间的相关系数。这里,我们计算Apple和Microsoft的年相关系数:

1
by_year.apply(lambda g: g['AAPL'].corr(g['MSFT']))
2003    0.480868
2004    0.259024
2005    0.300093
2006    0.161735
2007    0.417738
2008    0.611901
2009    0.432738
2010    0.571946
2011    0.581987
dtype: float64

示例:组级别的线性回归

顺着上一个例子继续,你可以用groupby执行更为复杂的分组统计分析,只要函数返回的是pandas对象或标量值即可。例如,我可以定义下面这个regress函数(利用statsmodels计量经济学库)对各数据块执行普通最小二乘法(Ordinary Least Squares,OLS)回归:

1
2
3
4
5
6
7
import statsmodels.api as sm
def regress(data, yvar, xvars):
Y = data[yvar]
X = data[xvars]
X['intercept'] = 1.
result = sm.OLS(Y, X).fit()
return result.params

现在,为了按年计算AAPL对SPX收益率的线性回归,执行:

1
by_year.apply(regress, 'AAPL', ['SPX'])
SPX intercept
2003 1.195406 0.000710
2004 1.363463 0.004201
2005 1.766415 0.003246
2006 1.645496 0.000080
2007 1.198761 0.003438
2008 0.968016 -0.001110
2009 0.879103 0.002954
2010 1.052608 0.001261
2011 0.806605 0.001514

透视表和交叉表

透视表(pivot table)是各种电子表格程序和其他数据分析软件中一种常见的数据汇总工具。它根据一个或多个键对数据进行聚合,并根据行和列上的分组键将数据分配到各个矩形区域中。在Python和pandas中,可以通过本章所介绍的groupby功能以及(能够利用层次化索引的)重塑运算制作透视表。DataFrame有一个pivot_table方法,此外还有一个顶级的pandas.pivot_table函数。除能为groupby提供便利之外,pivot_table还可以添加分项小计,也叫做margins。

1
tips.head()
total_bill tip smoker day time size tip_pct
0 16.99 1.01 No Sun Dinner 2 0.059447
1 10.34 1.66 No Sun Dinner 3 0.160542
2 21.01 3.50 No Sun Dinner 3 0.166587
3 23.68 3.31 No Sun Dinner 2 0.139780
4 24.59 3.61 No Sun Dinner 4 0.146808

假设我想要根据day和smoker计算分组平均数(pivot_table的默认聚合类型),并将day和smoker放到行上:

1
tips.pivot_table(index=['day', 'smoker'])
size tip tip_pct total_bill
day smoker
Fri No 2.250000 2.812500 0.151650 18.420000
Yes 2.066667 2.714000 0.174783 16.813333
Sat No 2.555556 3.102889 0.158048 19.661778
Yes 2.476190 2.875476 0.147906 21.276667
Sun No 2.929825 3.167895 0.160113 20.506667
Yes 2.578947 3.516842 0.187250 24.120000
Thur No 2.488889 2.673778 0.160298 17.113111
Yes 2.352941 3.030000 0.163863 19.190588

可以用groupby直接来做。现在,假设我们只想聚合tip_pct和size,而且想根据time进行分组。我将smoker放到列上,把day放到行上:

1
tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'], columns='smoker')
size tip_pct
smoker No Yes No Yes
time day
Dinner Fri 2.000000 2.222222 0.139622 0.165347
Sat 2.555556 2.476190 0.158048 0.147906
Sun 2.929825 2.578947 0.160113 0.187250
Thur 2.000000 NaN 0.159744 NaN
Lunch Fri 3.000000 1.833333 0.187735 0.188937
Thur 2.500000 2.352941 0.160311 0.163863
1
tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'])
size tip_pct
time day
Dinner Fri 2.166667 0.158916
Sat 2.517241 0.153152
Sun 2.842105 0.166897
Thur 2.000000 0.159744
Lunch Fri 2.000000 0.188765
Thur 2.459016 0.161301

还可以对这个表作进一步的处理,传入 margins=True 添加分项小计。这将会添加标签为All的行和列,其值对应于单个等级中所有数据的分组统计:

1
tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'], columns='smoker', margins=True)
size tip_pct
smoker No Yes All No Yes All
time day
Dinner Fri 2.000000 2.222222 2.166667 0.139622 0.165347 0.158916
Sat 2.555556 2.476190 2.517241 0.158048 0.147906 0.153152
Sun 2.929825 2.578947 2.842105 0.160113 0.187250 0.166897
Thur 2.000000 NaN 2.000000 0.159744 NaN 0.159744
Lunch Fri 3.000000 1.833333 2.000000 0.187735 0.188937 0.188765
Thur 2.500000 2.352941 2.459016 0.160311 0.163863 0.161301
All 2.668874 2.408602 2.569672 0.159328 0.163196 0.160803

要使用其他的聚合函数,将其传给aggfunc即可。例如,使用count或len可以得到有关分组大小的交叉表(计数或频率):

1
tips.pivot_table('tip_pct', index=['time', 'smoker'], columns='day', aggfunc=len, margins=True)
day Fri Sat Sun Thur All
time smoker
Dinner No 3.0 45.0 57.0 1.0 106.0
Yes 9.0 42.0 19.0 NaN 70.0
Lunch No 1.0 NaN NaN 44.0 45.0
Yes 6.0 NaN NaN 17.0 23.0
All 19.0 87.0 76.0 62.0 244.0

如果存在空的组合(也就是NA),你可能会希望设置一个fill_value:

1
2
tips.pivot_table('tip_pct', index=['time', 'size', 'smoker'],
columns='day', aggfunc='mean', fill_value=0)
day Fri Sat Sun Thur
time size smoker
Dinner 1 No 0.000000 0.137931 0.000000 0.000000
Yes 0.000000 0.325733 0.000000 0.000000
2 No 0.139622 0.162705 0.168859 0.159744
Yes 0.171297 0.148668 0.207893 0.000000
3 No 0.000000 0.154661 0.152663 0.000000
Yes 0.000000 0.144995 0.152660 0.000000
4 No 0.000000 0.150096 0.148143 0.000000
Yes 0.117750 0.124515 0.193370 0.000000
5 No 0.000000 0.000000 0.206928 0.000000
Yes 0.000000 0.106572 0.065660 0.000000
6 No 0.000000 0.000000 0.103799 0.000000
Lunch 1 No 0.000000 0.000000 0.000000 0.181728
Yes 0.223776 0.000000 0.000000 0.000000
2 No 0.000000 0.000000 0.000000 0.166005
Yes 0.181969 0.000000 0.000000 0.158843
3 No 0.187735 0.000000 0.000000 0.084246
Yes 0.000000 0.000000 0.000000 0.204952
4 No 0.000000 0.000000 0.000000 0.138919
Yes 0.000000 0.000000 0.000000 0.155410
5 No 0.000000 0.000000 0.000000 0.121389
6 No 0.000000 0.000000 0.000000 0.173706

pivot_table的参数说明请参见表

pandas高级应用

分类数据

通过使用它,提高性能和内存的使用率。

背景和目的

表中的一列通常会有重复的包含不同值的小集合的情况。我们已经学过了unique和value_counts,它们可以从数组提取出不同的值,并分别计算频率:

1
2
values = pd.Series(['apple', 'orange', 'apple', 'apple'] * 2)
values
0     apple
1    orange
2     apple
3     apple
4     apple
5    orange
6     apple
7     apple
dtype: object
1
pd.unique(values)
array(['apple', 'orange'], dtype=object)
1
pd.value_counts(values)
apple     6
orange    2
dtype: int64

许多数据系统(数据仓库、统计计算或其它应用)都发展出了特定的表征重复值的方法,以进行高效的存储和计算。在数据仓库中,最好的方法是使用所谓的包含不同值的维表(Dimension Table),将主要的参数存储为引用维表整数键:

就像 mysql 的分表存储

1
2
values = pd.Series([0, 1, 0, 0] * 2)
values
0    0
1    1
2    0
3    0
4    0
5    1
6    0
7    0
dtype: int64
1
2
dim = pd.Series(['apple', 'orange'])
dim
0     apple
1    orange
dtype: object

可以使用take方法存储原始的字符串Series:

1
dim.take(values)
0     apple
1    orange
0     apple
0     apple
0     apple
1    orange
0     apple
0     apple
dtype: object

这种用整数表示的方法称为分类或字典编码表示法。不同值得数组称为分类、字典或数据级。本书中,我们使用分类的说法。表示分类的整数值称为分类编码或简单地称为编码。

pandas的分类类型

pandas有一个特殊的分类类型,用于保存使用整数分类表示法的数据。

1
2
3
4
5
6
7
8
9
10
fruits = ['apple', 'orange', 'apple', 'apple'] * 2
N = len(fruits)
df = pd.DataFrame({'fruit': fruits,
'basket_id': np.arange(N),
'count': np.random.randint(3, 15, size=N),
'weight': np.random.uniform(0, 4, size=N)},
columns=['basket_id', 'fruit', 'count', 'weight'])
df
basket_id fruit count weight
0 0 apple 11 0.661412
1 1 orange 6 2.661072
2 2 apple 10 3.956839
3 3 apple 10 3.491835
4 4 apple 6 2.954149
5 5 orange 12 3.967850
6 6 apple 8 0.093289
7 7 apple 3 3.056569

这里,df['fruit'] 是一个Python字符串对象的数组。我们可以通过调用它,将它转变为分类:

1
2
fruit_cat = df['fruit'].astype('category')
fruit_cat
0     apple
1    orange
2     apple
3     apple
4     apple
5    orange
6     apple
7     apple
Name: fruit, dtype: category
Categories (2, object): [apple, orange]

fruit_cat的值不是NumPy数组,而是一个pandas.Categorical实例:

1
2
c = fruit_cat.values
c
[apple, orange, apple, apple, apple, orange, apple, apple]
Categories (2, object): [apple, orange]
1
type(c)
pandas.core.arrays.categorical.Categorical

分类对象有categories和codes属性:

1
c.categories
Index(['apple', 'orange'], dtype='object')
1
c.codes
array([0, 1, 0, 0, 0, 1, 0, 0], dtype=int8)

你可将DataFrame的列通过分配转换结果,转换为分类:

1
df['fruit'] = df['fruit'].astype('category')
1
df
basket_id fruit count weight
0 0 apple 11 0.661412
1 1 orange 6 2.661072
2 2 apple 10 3.956839
3 3 apple 10 3.491835
4 4 apple 6 2.954149
5 5 orange 12 3.967850
6 6 apple 8 0.093289
7 7 apple 3 3.056569
1
df.fruit
0     apple
1    orange
2     apple
3     apple
4     apple
5    orange
6     apple
7     apple
Name: fruit, dtype: category
Categories (2, object): [apple, orange]

你还可以从其它Python序列直接创建pandas.Categorical:

1
2
my_categories = pd.Categorical(['foo', 'bar', 'baz', 'foo', 'bar'])
my_categories
[foo, bar, baz, foo, bar]
Categories (3, object): [bar, baz, foo]

如果你已经从其它源获得了分类编码,你还可以使用from_codes构造器:

1
2
3
4
5
6
7
categories = ['foo', 'bar', 'baz']
codes = [0, 1, 2, 0, 0, 1]
my_cats_2 = pd.Categorical.from_codes(codes, categories)
my_cats_2
[foo, bar, baz, foo, foo, bar]
Categories (3, object): [foo, bar, baz]

与显示指定不同,分类变换不认定指定的分类顺序。因此取决于输入数据的顺序,categories数组的顺序会不同。当使用from_codes或其它的构造器时,你可以指定分类一个有意义的顺序:

1
2
ordered_cat = pd.Categorical.from_codes(codes, categories, ordered=True)
ordered_cat
[foo, bar, baz, foo, foo, bar]
Categories (3, object): [foo < bar < baz]

无序的分类实例可以通过as_ordered排序:

1
my_cats_2.as_ordered()
[foo, bar, baz, foo, foo, bar]
Categories (3, object): [foo < bar < baz]

用分类进行计算

来看一些随机的数值数据,使用pandas.qcut面元函数。它会返回pandas.Categorical,我们之前使用过pandas.cut,但没解释分类是如何工作的:

1
2
3
4
np.random.seed(12345)
draws = np.random.randn(1000)
draws[:5]
array([-0.20470766,  0.47894334, -0.51943872, -0.5557303 ,  1.96578057])

计算这个数据的分位面元,提取一些统计信息:

1
2
bins = pd.qcut(draws, 4)
bins
[(-0.684, -0.0101], (-0.0101, 0.63], (-0.684, -0.0101], (-0.684, -0.0101], (0.63, 3.928], ..., (-0.0101, 0.63], (-0.684, -0.0101], (-2.9499999999999997, -0.684], (-0.0101, 0.63], (0.63, 3.928]]
Length: 1000
Categories (4, interval[float64]): [(-2.9499999999999997, -0.684] < (-0.684, -0.0101] < (-0.0101, 0.63] < (0.63, 3.928]]

虽然有用,确切的样本分位数与分位的名称相比,不利于生成汇总。我们可以使用labels参数qcut,实现目的:

1
2
bins = pd.qcut(draws, 4, labels=['Q1', 'Q2', 'Q3', 'Q4'])
bins
[Q2, Q3, Q2, Q2, Q4, ..., Q3, Q2, Q1, Q3, Q4]
Length: 1000
Categories (4, object): [Q1 < Q2 < Q3 < Q4]
1
bins.codes[:10]
array([1, 2, 1, 1, 3, 3, 2, 2, 3, 3], dtype=int8)

加上标签的面元分类不包含数据面元边界的信息,因此可以使用groupby提取一些汇总信息:

1
2
bins = pd.Series(bins, name='quartile')
bins.head()
0    Q2
1    Q3
2    Q2
3    Q2
4    Q4
Name: quartile, dtype: category
Categories (4, object): [Q1 < Q2 < Q3 < Q4]
1
2
3
4
5
results = (pd.Series(draws)
.groupby(bins)
.agg(['count', 'min', 'max'])
.reset_index())
results
quartile count min max
0 Q1 250 -2.949343 -0.685484
1 Q2 250 -0.683066 -0.010115
2 Q3 250 -0.010032 0.628894
3 Q4 250 0.634238 3.927528

分位数列保存了原始的面元分类信息,包括排序:

1
results['quartile']
0    Q1
1    Q2
2    Q3
3    Q4
Name: quartile, dtype: category
Categories (4, object): [Q1 < Q2 < Q3 < Q4]

用分类提高性能

如果你是在一个特定数据集上做大量分析,将其转换为分类可以极大地提高效率。DataFrame列的分类使用的内存通常少的多。来看一些包含一千万元素的Series,和一些不同的分类:

1
2
3
4
N = 10000000
draws = pd.Series(np.random.randn(N))
labels = pd.Series(['foo', 'bar', 'baz', 'qux'] * (N // 4))

现在,将标签转换为分类:

1
categories = labels.astype('category')

这时,可以看到标签使用的内存远比分类多:

1
labels.memory_usage()
80000080
1
categories.memory_usage()
10000272

转换为分类不是没有代价的,但这是一次性的代价:

1
%time _ = labels.astype('category')
Wall time: 429 ms

GroupBy使用分类操作明显更快,是因为底层的算法使用整数编码数组,而不是字符串数组。

分类方法

包含分类数据的Series有一些特殊的方法,类似于Series.str字符串方法。它还提供了方便的分类和编码的使用方法。看下面的Series:

1
2
3
4
5
s = pd.Series(['a', 'b', 'c', 'd'] * 2)
cat_s = s.astype('category')
cat_s
0    a
1    b
2    c
3    d
4    a
5    b
6    c
7    d
dtype: category
Categories (4, object): [a, b, c, d]

特别的cat属性提供了分类方法的入口:

1
cat_s.cat.codes
0    0
1    1
2    2
3    3
4    0
5    1
6    2
7    3
dtype: int8
1
cat_s.cat.categories
Index(['a', 'b', 'c', 'd'], dtype='object')

假设我们知道这个数据的实际分类集,超出了数据中的四个值。我们可以使用set_categories方法改变它们:

1
2
3
actual_categories = ['a', 'b', 'c', 'd', 'e']
cat_s2 = cat_s.cat.set_categories(actual_categories)
cat_s2
0    a
1    b
2    c
3    d
4    a
5    b
6    c
7    d
dtype: category
Categories (5, object): [a, b, c, d, e]

虽然数据看起来没变,新的分类将反映在它们的操作中。例如,如果有的话,value_counts表示分类:

1
cat_s.value_counts()
d    2
c    2
b    2
a    2
dtype: int64
1
cat_s2.value_counts()
d    2
c    2
b    2
a    2
e    0
dtype: int64

在大数据集中,分类经常作为节省内存和高性能的便捷工具。过滤完大DataFrame或Series之后,许多分类可能不会出现在数据中。我们可以使用remove_unused_categories方法删除没看到的分类:

1
2
cat_s3 = cat_s[cat_s.isin(['a', 'b'])]
cat_s3
0    a
1    b
4    a
5    b
dtype: category
Categories (4, object): [a, b, c, d]
1
cat_s3.cat.remove_unused_categories()
0    a
1    b
4    a
5    b
dtype: category
Categories (2, object): [a, b]

可用的分类方法

为建模创建虚拟变量

当你使用统计或机器学习工具时,通常会将分类数据转换为虚拟变量,也称为one-hot编码。这包括创建一个不同类别的列的DataFrame;这些列包含给定分类的1s,其它为0。

1
2
cat_s = pd.Series(['a', 'b', 'c', 'd'] * 2, dtype='category')
cat_s
0    a
1    b
2    c
3    d
4    a
5    b
6    c
7    d
dtype: category
Categories (4, object): [a, b, c, d]

pandas.get_dummies 函数可以转换这个分类数据为包含虚拟变量的DataFrame:

1
pd.get_dummies(cat_s)
a b c d
0 1 0 0 0
1 0 1 0 0
2 0 0 1 0
3 0 0 0 1
4 1 0 0 0
5 0 1 0 0
6 0 0 1 0
7 0 0 0 1

GroupBy高级应用

分组转换

在分组操作中学习了apply方法,进行转换。还有另一个transform方法,它与apply很像,但是对使用的函数有一定限制:

  1. 它可以产生向分组形状广播标量值
  2. 它可以产生一个和输入组形状相同的对象
  3. 它不能修改输入
1
2
df = pd.DataFrame({'key': ['a', 'b', 'c'] * 4, 'value': np.arange(12.)})
df
key value
0 a 0.0
1 b 1.0
2 c 2.0
3 a 3.0
4 b 4.0
5 c 5.0
6 a 6.0
7 b 7.0
8 c 8.0
9 a 9.0
10 b 10.0
11 c 11.0

按键进行分组:

1
2
g = df.groupby('key').value
g.mean()
key
a    4.5
b    5.5
c    6.5
Name: value, dtype: float64

假设我们想产生一个和 df['value'] 形状相同的Series,但值替换为按键分组的平均值。我们可以传递函数 lambda x: x.mean() 进行转换:

1
g.transform(lambda x: x.mean())
0     4.5
1     5.5
2     6.5
3     4.5
4     5.5
5     6.5
6     4.5
7     5.5
8     6.5
9     4.5
10    5.5
11    6.5
Name: value, dtype: float64

对于内置的聚合函数,我们可以传递一个字符串假名作为GroupBy的agg方法:

1
g.transform('mean')
0     4.5
1     5.5
2     6.5
3     4.5
4     5.5
5     6.5
6     4.5
7     5.5
8     6.5
9     4.5
10    5.5
11    6.5
Name: value, dtype: float64

与apply类似,transform的函数会返回Series,但是结果必须与输入大小相同。举个例子,我们可以用lambda函数将每个分组乘以2:

1
g.transform(lambda x: x * 2)
0      0.0
1      2.0
2      4.0
3      6.0
4      8.0
5     10.0
6     12.0
7     14.0
8     16.0
9     18.0
10    20.0
11    22.0
Name: value, dtype: float64

看一个由简单聚合构造的的分组转换函数:
我们用transform或apply可以获得等价的结果:

1
2
3
def normalize(x):
return (x - x.mean()) / x.std()
g.transform(normalize)
0    -1.161895
1    -1.161895
2    -1.161895
3    -0.387298
4    -0.387298
5    -0.387298
6     0.387298
7     0.387298
8     0.387298
9     1.161895
10    1.161895
11    1.161895
Name: value, dtype: float64
1
g.apply(normalize)
0    -1.161895
1    -1.161895
2    -1.161895
3    -0.387298
4    -0.387298
5    -0.387298
6     0.387298
7     0.387298
8     0.387298
9     1.161895
10    1.161895
11    1.161895
Name: value, dtype: float64

分组的时间重采样

对于时间序列数据,resample方法从语义上是一个基于内在时间的分组操作。

1
2
3
4
5
6
N = 15
times = pd.date_range('2017-05-20 00:00', freq='1min', periods=N)
df = pd.DataFrame({'time': times, 'value': np.arange(N)})
df
time value
0 2017-05-20 00:00:00 0
1 2017-05-20 00:01:00 1
2 2017-05-20 00:02:00 2
3 2017-05-20 00:03:00 3
4 2017-05-20 00:04:00 4
5 2017-05-20 00:05:00 5
6 2017-05-20 00:06:00 6
7 2017-05-20 00:07:00 7
8 2017-05-20 00:08:00 8
9 2017-05-20 00:09:00 9
10 2017-05-20 00:10:00 10
11 2017-05-20 00:11:00 11
12 2017-05-20 00:12:00 12
13 2017-05-20 00:13:00 13
14 2017-05-20 00:14:00 14

这里,我们可以用time作为索引,然后重采样:

1
df.set_index('time').resample('5min').count()
value
time
2017-05-20 00:00:00 5
2017-05-20 00:05:00 5
2017-05-20 00:10:00 5

假设DataFrame包含多个时间序列,用一个额外的分组键的列进行标记:

1
2
3
4
df2 = pd.DataFrame({'time': times.repeat(3),
'key': np.tile(['a', 'b', 'c'], N),
'value': np.arange(N * 3.)})
df2.head()
time key value
0 2017-05-20 00:00:00 a 0.0
1 2017-05-20 00:00:00 b 1.0
2 2017-05-20 00:00:00 c 2.0
3 2017-05-20 00:01:00 a 3.0
4 2017-05-20 00:01:00 b 4.0
1
2
echo-ding wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!